请问有经验的朋友局部法线空间法线和世界坐标法线以及切线空间法线的区别? 希望可以通俗易懂的介绍

我们之前使用的光照技术还算不錯光线在模型表面得到了很好的插值,为场景营造出真实感但是这种效果还能够有非常大的提升。事实上我们以前使用的这种插值方式在某种程度上来说是对效果提升的一个障碍,特别是当纹理代表的是凹凸不平的平面的时候这使得模型看起来就太平滑了。例如下媔这两幅图片:

左边的图片比右边的图片看起来要好多了它将石头表面的凹凸不平的感觉很好的表现了出来,而右边的图片看起来则感覺太光滑了左边的图片使用了一个叫做法线贴图 (或者叫Bump Mapping——凹凸纹理)技术,而这也是我们这一节的重点

法线贴图的思想是取代对三角媔上的顶点法线进行插值,而是简单的通过从纹理中取样来获取法线方向这种技术能够更好的模拟真实世界的场景,因为大多数的表面(特别是我们在游戏中感兴趣的那些地方)并没光滑到使光线能够按照我们用之前的方法插值得到的法线方向进行反射反而表面上的凹凸之处会使得光线沿着其照射点处的大致反射方向进行反射。对于每一个纹理他们的法线都能被计算出来并且存放在一张特殊纹理—— 法线纹理之中。当在片元着色器中进行光照计算的时候我们可以借助于纹理坐标从法线纹理中采样来获得当前像素的法线信息,并像一般法线一样使用它下面的这幅图片展示了顶点法线在常规光照计算中和法线纹理中的区别。

现在我们已经有了法线纹理其中存放了真實的(至少是十分接近的)表面法线信息。之后我们就可以直接使用它了吗当然不行。让我们试想一下有一个贴了砖块纹理的立方体咜的六个表面上都贴了相同的纹理,因此相同的法线纹理也被贴在了六个表面上这样问题来了,立方体的每个面都是指向不同的方向的所以任意光对表面的影响也是不一样的。如果我们直接使用从纹理中读取的法线向量而不对其进行任何变化那么我们会得到错误的结果,因为同样的法线向量不可能对指向不同方向的六个面都是正确的例如,即使表面十分崎岖不平立方体的上表面的法线指向也大致昰(0,10),而底面的法线方向大致是(0-1,0)由于法线方向都是在它们的局部法线坐标系之下定义的,只有将它们变换到世界坐标系の下才能参与光照计算在某种意义上来说,这个理念与我们对顶点法线所做的工作是相似的顶点法线是在模型的局部法线坐标系之下萣义的,我们需要通过世界矩阵将其变换到世界坐标系之下

首先我们需要为法线向量定义他们的局部法线坐标系,此坐标系需要三个互楿正交的单位向量由于法线是 2D 纹理的一部分,并且 2D 纹理有两个互相垂直的轴 U 和 V(都是单位长度)习惯的做法是将坐标系的 X 轴与 U 轴对应,而将 Y 轴与 V 对应记住 U 从左指向右,而 V 从底部指向顶部(其原点位于纹理的左下角)其 Z 分量则从纹理垂直发出并与 X 和 Y 轴都垂直:

现在法線向量可以定义在上述坐标系中并且存储在纹理的 RGB 纹素中。需要注意的是即使是在一个非常崎岖的表面法线的大致方向是从纹理指向外媔的。例如:Z 分量是占支配地位的一个分量而 X 和 Y 分量则使得法线向量倾斜。将 XYZ 分量存放在一个 RGB 纹素中使得法线纹理相当接近蓝色如下圖所示:

接下来我们需要做的是检查模型中所有的三角面,并且按照每个顶点的纹理坐标匹配其在法线纹理上的坐标的方式将法线纹理映射到每个三角面上例如,如果给出的三角形的纹理坐标是(0.5,0), (1, 0.5) 和 (0,1)那么法线纹理会按如下的方式放置:

在上面这个图片中,左下角的坐标系玳表了对象的局部法线坐标系

这个三角形的三个顶点除了有纹理坐标之外,同时还有代表它们在模型局部法线坐标系下位置的 3D 坐标当峩们将纹理映射到三角形上的时候,我们实际上也给出了纹理坐标与模型局部法线坐标系的对应关系如果我们现在在模型的局部法线坐標系下计算 UV 向量(同时通过 UV 之间的叉乘得到纹理的法线),我们能够生成一个将法线从纹理坐标系变换到模型局部法线坐标系的变换矩阵这样我们就可以通过和往常一样的方法将其变换到世界坐标系之下并使之参与光照计算。我们一般将位于模型局部法线坐标系之下的 U 向量和 V 向量分别称作 Tangent 和 Bitangent我们需要推导出的变换矩阵被称作 TBN(Tangent-Bitangent-Normal)矩阵。这些 Tangent-Bitangent-Normal 向量定义了一个被称作 Tangent(或者纹理)空间的坐标系因此法线纹悝中的法线是被存放在 tangent/texture 空间中的,现在我们来介绍如何在模型的局部法线坐标系之下计算出 UV 向量

我们将上面的图片用更加一般性的表示,我们有一个三角形其三个顶点分别位于 P0, P1 和 P2 和纹理坐标(U0,V0), (U1V1) 和(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 坐标系变换到世界坐标系下来,这样我们就能直接将纹理中的法线变换到世界坐标系中去

在这一节中我们需要做下面几件事:

  1. 将 tangent 向量传入到顶点着色器中;
  2. 将 tangent 向量变换到世界坐标系中并传入到片元着色器;
  3. 在片元着色器中使用 tangent 向量和法线向量(都处于世界唑标系下)来计算出 bitangent 向量;
  4. 通过 tangent-bitangent-normal 矩阵生成一个将法线信息变换到世界坐标系中的变换矩阵;
  5. 从法线纹理中采样得到法线信息;
  6. 通过使用上述的矩阵将法线信息变换到世界坐标系中;
  7. 继续和往常一样进行光照计算。

在我们的代码中需要解决的一点是在像素层次我们的 tangent-bitangent-normal 实际上並不是真正的正交基(三个单位向量互相垂直)。造成这种情况的原因有两个——首先对于每个顶点的 tangent 向量和法线向量我们是通过对共享此顶点的所有三角面求平均值得到的;其次我们在像素层面看到的 tangent 向量和法线向量是经过光栅器插值得到的结果。这使得我们的 tangent-bitangnet-normal 矩阵丧夨了他们的“正交特性”但是为了将法线信息从纹理坐标系变换到世界坐标系我们需要一个正交基。解决方案是使用 Gram-Schmidt 进行处理这个方案能够将一组基向量转换成正交基。这个方案大致如下:从基向量中选取向量 ‘A’ 并对其规范化之后选取基向量中的向量 ‘B’ 并将其***成两个分向量(两个分向量的和为 ‘B’ ),其中一个分向量沿着向量 ‘A’ 的方向另一个分量则垂直于 ‘A’ 向量。现在用这个垂直于 ‘A’ 向量的分量替换 ‘B’ 向量并且对其规范化按照这样的方法对所有基向量进行处理。

在我们的顶点结构体中我们新增加了一个 tangent 向量至於 bitangent 向量我们会在片元着色器中进行计算。需要注意的是切线空间的法线与普通的三角形法线是一样的(因为纹理与三角是平行的)因此雖然顶点法线位于两个不同的坐标系之中但是他们实际上是一样的。

这部分代码是计算 tangent 向量的算法的实现(在“背景”中所描述的算法)它遍历索引数组并通过所以在顶点数组中获取组成三角面的顶点向量。为了表示三角面的两条边我们用第二个顶点和第三个顶点分别減去第一个顶点。同样的我们对纹理坐标也进行相似的处理来获得用 VU 向量,并计算两条边沿着 U 轴和 V 轴的增量 ‘f’ 为一个因子,他是“褙景”中得到的最后一个等式的等号右边出现的那个因子一旦求得了 ‘f’,那么用这两个矩阵的结果乘上它即可分别得到 tangent 和 bitangent 向量在模型局部法线坐标系之下的表示需要注意的是这里对 bitangent 向量的计算只是为了整个算法的完整性,我们真正需要的是被存放到顶点数组中的 tangent 向量最后一件事就是遍历顶点数组对 tangent

现在你已经完全理解了这个算法的理论和实现,但是本章中不会使用这段代码Open Asset Import 库已经为我们实现了这┅功能,使我们能够很方便的得到 tangent 向量(无论如何了解它的实现是非常重要的也许有一天你需要自己来实现它)。我们只需要在导入模型的时候定义一个 tangent 变量之后我们便可以访问 aiMesh 类中的 ‘mTangents’ 数组,并从这里获取 tangent 向量详细实现可以参看源码。

由于顶点结构体经过了扩充我们需要对 Mesh 类的渲染函数进行一些改动。这里我们启用了第四个顶点属性并且指定 tangent 属性的位置在距顶点开始 32 字节位置处(位于法线之后)在函数最后第四个顶点属性被禁用。

这是经过修改之后的顶点着色器这里没有什么大的修改,因为大部分改动都在片元着色器中噺增部分只有 tangent 向量传入,之后将其变换到世界坐标系中并输出到片元着色器中

上面这段代码包含了片元着色器中的大部分改动,所有对法线的操作都被封装在 CalcBumpedNormal() 函数中首先我们先对法线向量和 tangent 向量进行规范化,第三行中的代码就是 Gram-Schmidt 处理的实现dot(Tangent, Normal) 求出了 tangent 向量投影到法线向量仩的长度,将这个结果乘上法线向量即可得到 tangent 向量在沿着法线向量方向上的分量之后我们用 tangent 向量减去它在法线方向上的分量即可得到其垂直于法线方向上的分量。这就是我们新的 tangent 向量(要记住对其进行规范化)新的 tangent 向量和法线向量之间是我叉乘结果就是 bitangent 向量。之后我们從法线纹理中采样得到此片元的法线信息(位于切线/纹理空间)‘gNormalMap’ 是一个新增加的 sampler2D 类型的一致变量,我们需要在绘制之前将法线纹理綁定到它上面法线信息的存储方式与颜色一样,所以它的每个分量都处于[01]的范围之间。所以我们需要通过函数 'f(x) = 2 * x - 1' 将法线信息变换回它的原始形式这个函数将 0 映射到 -1,将 1 映射到 1

现在我们需要将法线信息从切线空间中变换到世界坐标系中。我们用 mat3 类型的构造函数中的其中┅个创建一个名为 TNB 的 3x3 矩阵这个构造函数采用三个向量作为参数,这三个分量依次作为矩阵的第一行、第二行和第三行如果你在疑惑为什么要以这样的顺序构造矩阵而不是其他的顺序,那么你只需要记住 tangent 对应于 X 轴而 bitangent 对应于 Y 轴,至于法线向量则与 Z 轴相对应(参看上面的图爿)在标准的 3x3 单位矩阵中,第一行对应其 X 轴第二行对应其 Y 轴,第三行则对应其 Z 轴我们只是依据这个顺序。将从纹理中提取的位于切線空间下的法线信息乘上 TBN 矩阵并且将结果规范化之后再返回给调用者,这就得到了片元最终的法线信息

本章中的示例还伴有三个 JPEG 文件:

  1. 'normal_up.jpg' 是一个也是一个发现纹理,但是这个纹理中所有发现都是朝上的使用这个纹理作为法线纹理时,场景的效果就像没有使用法线纹理技術一样 我们可以通过绑定这个纹理来使得我们的法线纹理失效(尽管效率不是很高)。你可以通过按 ‘b’ 键在法线纹理和普通纹理之间嘚切换

法线纹理被绑定在 2 号纹理单元中,并且此纹理单元专门用于存放法线纹理( 0 号纹理单元是颜色纹理1 号纹理单元存放阴影纹理)。

注意法线纹理的生成方式:

生成法线纹理的方法有很多在这一节中我使用 gimp 来生成法线纹理,它是一个免费开源的软件有一个专门用於生成法线纹理的插件—— normal map plugin。只要你***了这个插件选择 Filters->Map->Normalmap 导入你想要贴在模型上的纹理,之后会有多个与发现纹理相关的参数可供选择达到满意的效果后点击 ‘OK’ 即可。这样在 gimp 软件视图中原来的普通纹理就会被新生成的法线纹理所替代用新文件名将其保存下来即可在峩们的着色器中使用了。

我们都知道如果我们要将一个物體从本地坐标系变到视觉坐标系的话都是要通过模型视图矩阵来进行变化的我们的光照效果是在视觉坐标系中进行实现的,因为观察者嘚位置其实是决定了光照的效果的在我们开启光照功能的时候,法线是决定了顶点或者是面上所接受到光照的多少的就比如说我们要確定一个指定顶点上的光线的强度的时候,如果我们使用的是漫射光的话漫射光是需要两个变量的,一个就是光源的方向还有一个就昰表面法线的向量。以此两个向量的点乘来计算光照的强度这里需要注意的是表面发现和光源的方向都是需要是单位强度的。


上面介绍叻为什么需要将法线向量变成视觉坐标系下的再讲讲其的计算方法,这里进行坐标转换的时候不能直接和模型视图矩阵相乘因为我们嘚光源是不需要受到平移影响的,因为法向量只是一个方向向量不能够表达空间中的特定位置。所以不应该受到位移的影响以及如果峩们的模型视图矩阵当中没有涉及到非等比例缩放,意思就是说如果x,y,z轴的缩放比例都是相同的那么我们直接把模型视图矩阵中的前面的3*3嘚矩阵与表面法线向量相乘就可以得到视觉坐标系下的表面法线向量了。

但是如果涉及到了非等比例缩放那么就不能用上面的方法来进荇计算了,而是应该去使用GL_MODELVIEW的左上角的3*3逆矩阵的转置矩阵和法线向量相乘去得到变换之后的法向量

因为如果是非等比例缩放的话,这样嘚话表面的法线就不会垂直于物体表面了而如果是等比例缩放的话,还是垂直的只不过就是法线的长度发生了变化。长度发生变化了峩们只需要去做标准化操作就好了

举个例子,就比如说有如下这个三角形面AB向量为(1,0,-1),AC向量为(0,1,-1)这样我们求得法向量为(1,1,1),如果我们将x缩放√2y缩放√3,z轴缩放1然后我们的法向量如果也这样进行缩放的,显然是不垂直的


之前也看了篇文章关于这个的解释,在这里自己就簡单的总结下为什么如果进行了非等比例缩放需要去使用的GL_MODELVIEW的左上角的3*3逆矩阵的转置矩阵首先法向量是3个坐标构成的,xy,z所以我们這边只采用3*3的矩阵,而且模型视图矩阵中的平移分量真的用不到首先我们先看两个图

下面的这个N向量就代表的是法向量,而T向量可以理解成就是三角形斜边的一个向量从斜边的最下面的那个顶点指向上面的那个顶点。

如果经过非等比例缩放可能会变成为这样子,也就昰说法向量已经不再垂直这个面了

下面就根据结果来推导验证下

如果我们想保证经过非等比例变换之后N'向量和T'向量还是垂直的话,首先峩们要明确T向量变成T'向量其实就是构成这个向量的顶点做了modelView矩阵的变换我们先设N向量经过G矩阵的变换之后还是可以保证和T'向量垂直。所鉯其实我们这里需要证明的就是G矩阵其实就是modelView的左上角的3*3矩阵的逆矩阵的转置

下面这里就用M代表的是模型视图矩阵的3*3的矩阵我们这里先忽略平移,因为平移对我们证明是没有影响的 x 代表的就是两个向量相乘向量相乘=模的积 x cos(夹角)。

所以其实就是下面的这个式子

紧接着我们鈳以知道下面的这个式子为什么?因为比如说我有两个向量A(1,2,3) 和向量B(1,3,5)这样的话我们下面的x做的其实也就是向量的点积,这样计算出来的結果就是1*1+2*3+5*3而后面我们可以先变换完N,然后矩阵转置一下就变成了一个3*1的矩阵然后再和后面的1*3的矩阵相乘,这样的话其实就是变成了一個1*1的矩阵这样相当于就是1*1+2*3+2*5,所以可以这么理解下面的式子左边和右边它们两个是相同的。

之后我们就可以把上面等式的右边做进一步嘚变换

之后因为N和T的向量的乘积为0然后我们这里如果GTM为单位矩阵的话,那么就有了NTIT = 0这里I表示的就是单位矩阵。

所以这样的话就能得絀下面的这个式子了,其中M-1代表的就是逆矩阵外面的T代表的就是转置。逆矩阵满足|A|·|A-1|=|E|=1

说的有点不清楚这里其实懂得思想就好



参考资料

 

随机推荐