学习OpenGL。 第5.5课-法线映射

OGL3

法线贴图


我们使用的所有场景均由多边形组成,而多边形又由数百,数千个绝对平坦的三角形组成。 由于提供了在这些扁平三角形上应用二维纹理的附加细节,我们已经设法稍微增加了场景的真实感。 纹理有助于隐藏以下事实:场景中的所有对象只是许多小三角形的集合。 一项伟大的技术,但它的无限可能是无限的:当接近任何表面时,所有人都清楚它是由平坦的表面组成的。 大多数真实物体不是完全平坦的,并且显示出很多浮雕细节。


例如,以砖砌为例。 它的表面非常粗糙,显然不能用平面表示:在它的凹处上有水泥和许多小细节,例如孔和裂缝。 如果我们在有灯光的情况下分析模仿砖砌的场景,那么很容易消除表面浮雕的幻觉。 以下是这样一个场景的示例,其中包含带有砖石纹理的平面和一个点光源:


如您所见,照明根本不考虑该表面假定的浮雕细节:所有小的水泥裂缝和空腔与表面的其余部分是无法区分的。 可以使用镜面光泽度贴图来限制对表面凹部中某些细节的照明。 但这看起来更像是一个肮脏的hack,而不是一个可行的解决方案。 我们需要的是一种为照明方程式提供表面微浮雕上的数据的方法。
在我们已知的光照方程式的上下文中,请考虑以下问题:在什么条件下将表面照亮为完全平坦? 答案与表面法线有关。 从照明算法的角度来看,有关表面形状的信息仅通过法向矢量传输。 由于法线矢量在上面显示的表面上的每个位置都是恒定的,因此照明也变得均匀,对应于平面。 但是,如果我们不仅将属于该对象的所有片段的法线常数传递给照明算法,而且将每个片段的法线唯一性传递给照明算法,该怎么办? 因此,法线向量将根据表面形貌而略有变化,这将对表面复杂性产生更令人信服的错觉:


通过使用片段不同的法线,照明算法将认为该表面由许多垂直于其法向矢量的微观平面组成。 结果,这将大大增加对象的纹理。 对片段而不是对整个表面应用唯一的法线的技术-这是法线贴图凹凸贴图 。 应用于已经熟悉的场景:


由于性能成本非常适中,您可以看到视觉复杂性的惊人增加。 由于我们所有照明模型的变化都只是在每个片段中都提供唯一的法线,因此没有更改任何计算公式。 仅在输入处,而不是在插入的法线处,当前片段的法线才到达表面。 所有相同的照明方程式将完成其余的工作,以营造一种浮雕的感觉。

法线贴图


因此,事实证明,我们需要为照明算法提供每个片段唯一的法线。 我们将使用在漫反射和镜面反射纹理中已经很熟悉的方法,并使用通常的2D纹理在表面上的每个点存储法线数据。 不要惊讶,纹理对于存储法向矢量也非常有用。 然后,我们只需要从纹理中选择,恢复法线向量并进行光照计算即可。

乍一看,如何将矢量数据保存为常规纹理(通常用于存储颜色信息)可能并不十分清楚。 但是请再想一想:RGB颜色三合会本质上是三维向量。 以类似的方式,您可以将XYZ法线向量的分量保存为相应的颜色分量。 法线向量的分量值位于区间[-1,1]中,因此需要额外转换为区间[0,1]:

vec3 rgb_normal = normal * 0.5 + 0.5; //   [-1,1]  [0,1] 

将法线向量减少到RGB颜色分量的空间将使我们能够将法线向量保存在纹理中,该纹理是基于建模对象的真实浮雕而获得的,并且对于每个片段都是唯一的。 对于相同的砖砌,此类纹理的示例(法线贴图):


有趣的是注意到此法线贴图的蓝色色调(几乎所有法线贴图都具有类似的色调)。 发生这种情况是因为所有法线都大致沿oZ轴定向,该轴由坐标三元组(0,0,1)表示,即 以三色组合的形式-纯蓝色。 在某些区域,由于法线偏离正半轴oZ导致色相的微小变化,这对应于不平坦的地形。 因此,您可以看到在每块砖的上边缘,纹理呈现出绿色。 这是合乎逻辑的:在砖的上表面,法线应更朝向oY(0,1,0)轴定向,该轴对应于绿色。

对于测试场景,采取面向正半轴oZ的平面,并为其使用以下漫反射贴图法线贴图
请注意,链接和上图中的法线图不同。 在文章中,作者相当随意地提到了差异的原因,将自己局限于建议转换的法线贴图,以便在纹理平面局部的系统中,绿色分量指示“向下”而不是“向上”。
如果您更详细地看,则两个因素在这里相互作用:
  • 区别在于在客户端内存和OpenGL纹理内存中如何处理纹理像素
  • 法线贴图有两种表示法。 按照惯例,有两个阵营:DirectX风格和OpenGL风格

关于法线贴图符号,历史上很熟悉,有两个阵营:DirectX和OpenGL。


显然,它们不兼容。 稍加思考,您就可以理解DirectX认为切线空间是左手的,而OpenGL是右手的。 滑动我们的应用程序的X法线贴图而未做任何更改将导致不正确的照明,并且并不总是立即清除它是不正确的。 最值得注意的是,OpenGL格式的凸起成为DirectX的缩进,反之亦然。
至于寻址:将纹理文件中的数据加载到内存中,我们假设第一个纹理像素是图像的左上纹理像素。 为了在应用程序内存中表示纹理数据,通常是这样。 但是OpenGL使用不同的纹理坐标系:为此,第一个纹理像素位于左下角。 为了获得正确的纹理,通常在一个或另一个图像文件加载器的代码中沿Y轴翻转图像。 对于课程中使用的Stb_image,您需要添加一个复选框

 stbi_set_flip_vertically_on_load(1); 

有趣的是,在照明方面正确显示了两个选项:OpenGL标记中的法线贴图打开了Y反射或DirectX标记中的法线贴图关闭了Y反射两种情况下,照明都可以正常工作,不同之处仅在于沿轴的纹理的倒数是的



注意事项 反式

因此,加载两个纹理,绑定到纹理块并渲染准备好的平面,并考虑到片段着色器代码的以下修改:

 uniform sampler2D normalMap; void main() { //         [0,1] normal = texture(normalMap, fs_in.TexCoords).rgb; //      [-1,1] normal = normalize(normal * 2.0 - 1.0); [...] //  ... } 

在这里,我们将RGB值空间的逆变换应用于完整的法向矢量,然后将其简单地用于著名的Blinn-Fong照明模型中。

现在,如果您慢慢更改场景中光源的位置,您会感觉到法线贴图所提供的表面浮雕的错觉:


但是仍然存在一个问题,这极大地缩小了法线贴图的可能使用范围。 如前所述,法线贴图的蓝色色调暗示纹理中的所有矢量平均沿正轴oZ定向。 在我们的场景中,这不会造成问题,因为平面的法线也与oZ对齐。 但是,如果我们改变平面在场景中的位置,使其法线与正轴oY对齐,会发生什么?


照明完全错误! 原因很简单:地图中的法线仍返回沿正半轴oZ定向的矢量,尽管在这种情况下,它们应沿表面法线的正半轴oY方向定向。 同时,照明的计算就像是定位曲面的法线一样,就像平面仍朝向正半轴oZ定向一样,这会得出错误的结果。 下图更清楚地显示了从地图读取的法线相对于表面的方向:


可以看出,法线通常沿正半轴oZ对齐,尽管它们应该已经沿法向与沿正半轴oY定向的表面对齐。
一种可能的解决方案是针对所考虑的表面的每个方向设置单独的法线贴图。 对于一个多维数据集,将需要六个法线贴图,但是对于更复杂的模型,可能的方向数量可能太高并且不适合实现。

还有另一种在数学上更复杂的方法,该方法可以在不同的坐标系中计算照明:这样,其中的法向矢量始终与正半轴oZ大致重合。 然后将照明计算所需的其他矢量转换为该坐标系。 这种方法可以将一个法线贴图用于对象的任何方向。 这种特定的坐标系称为切线空间切线空间

切线空间


应当指出,法线图中的法线向量直接在切线空间中表示,即 在这样的坐标系中,法线始终始终近似指向正半轴oZ的方向。 切线空间定义为局部于三角形平面的坐标系,每个法向矢量都定义在该坐标系内。 您可以将这个系统想象为法线贴图的局部坐标系:无论曲面的最终方向如何,其中的所有矢量都朝着正半轴oZ方向给出。 使用专门准备的变换矩阵,可以将法线向量从该局部切线坐标系变换为世界或视图坐标,并根据带纹理的表面的最终位置对其进行定向。
考虑不正确使用法线贴图的前一个示例,其中平面沿正轴oY定向。 由于法线贴图是在切线空间中定义的,因此调整选项之一是计算从切线空间到法线的过渡矩阵,以使法线贴图的方向垂直于曲面。 这将导致法线沿着正轴oY对齐。 切线空间的显着特性是,通过计算这样的矩阵,我们可以将法线重新定向到任何表面及其方向。

这样的矩阵缩写为TBN ,它是向量TangentBitangentNormal的三元组名称的缩写。 我们需要找到这三个向量,以便形成这个基础变化矩阵。 这样的矩阵使矢量从切线空间过渡到其他空间,并且为形成该矩阵,需要三个相互垂直的矢量,其方向对应于法线贴图平面的方向。 这是向上,向右和向前的方向的矢量,这是我们从虚拟摄像机上的课程所熟悉的一组。
有了最高的向量,一切马上就清晰了-这是我们的正常向量。 右向量和正向量分别称为切线和切线。 下图给出了它们在平面上的相对位置的概念:


切线和双切线的计算没有法线向量的计算那么明显。 在图中,您可以看到法线的切线和切线图的方向与指定曲面的纹理坐标的轴对齐。 这个事实是计算这两个向量的基础,这将需要一些数学技能。 看图片:


沿三角形边缘的纹理坐标的变化 E2指定为 \三U2 DeltaV2以与切向量相同的方向表示 T和正切 B。 基于这个事实,您可以表达三角形的边缘 E1E2以切线和双切线向量的线性组合的形式:

E1= DeltaU1T+ DeltaV1B


E2= DeltaU2T+ DeltaV2B


转换成按位记录,我们得到:

E1xE1yE1z= DeltaU1TxTyTz+ DeltaV1BxByBz


E2xE2yE2z= DeltaU2TxTyTz+ DeltaV2BxByBz


E计算为两个向量之差的向量,并且 \三U\三V作为纹理坐标的差异。 仍然需要在两个方程中找到两个未知数:切线 T和偏见 B。 如果您回顾了代数课程,您就会知道,这样的条件可以解决 T和为 B
方程的最后一种给定形式允许我们以矩阵乘法的形式重写它:

\开bmatrixE1xE1yE1zE2xE2yE2z endbmatrix=\开bmatrix DeltaU1 DeltaV1 DeltaU2 DeltaV2\结bmatrix\开bmatrixTxTyTzBxByBz\结bmatrix


尝试在您的脑海中进行矩阵乘法以确保记录正确。 以矩阵形式编写系统可以更轻松地了解查找方法 TB。 将方程式的两边乘以  DeltaU DeltaV

\开bmatrix DeltaU1 DeltaV1 DeltaU2 DeltaV2 endbmatrix1 beginbmatrixE1xE1yE1zE2xE2yE2z endbmatrix=\开bmatrixTxTyTzBxByBz endbmatrix


我们有一个决定 TB,但是,这需要计算纹理坐标变化的逆矩阵。 我们将不讨论计算逆矩阵的详细信息-逆矩阵的表达式看起来像是与原始矩阵和伴随矩阵的行列式成反比的乘积:

\开bmatrixTxTyTzBxByBz\结bmatrix= frac1 DeltaU1 DeltaV2 DeltaU2 DeltaV1\开bmatrix DeltaV2 DeltaV1 DeltaU2 DeltaU1 endbmatrix beginbmatrixE1xE1yE1zE2xE2yE2z\结bmatrix


该表达式是计算切向量的公式 T和正切 B基于三角形的面的坐标和相应的纹理坐标。
如果上述数学计算的本质使您难以理解,请不要担心。 如果您了解我们根据三角形顶点的坐标及其纹理坐标(因为纹理坐标也属于切线空间)获得了切线和斜切线-这已经是成功的一半。

切线和切线的计算


在本课程的示例中,我们采用了一个简单的平面,正向正半轴oZ。 现在,我们将尝试使用切线空间实现法线贴图,以便能够根据需要在示例中定向平面,而不会破坏法线贴图效果。 使用以上计算,我们可以手动找到所考虑曲面的切线和双切线。
我们假设该平面由具有纹理坐标的以下顶点组成(两个三角形由矢量1、2、3和1、3、4给出):

 //   glm::vec3 pos1(-1.0, 1.0, 0.0); glm::vec3 pos2(-1.0, -1.0, 0.0); glm::vec3 pos3( 1.0, -1.0, 0.0); glm::vec3 pos4( 1.0, 1.0, 0.0); //   glm::vec2 uv1(0.0, 1.0); glm::vec2 uv2(0.0, 0.0); glm::vec2 uv3(1.0, 0.0); glm::vec2 uv4(1.0, 1.0); //   glm::vec3 nm(0.0, 0.0, 1.0); 

首先,我们计算描述三角形面的矢量以及纹理坐标的增量:

 glm::vec3 edge1 = pos2 - pos1; glm::vec3 edge2 = pos3 - pos1; glm::vec2 deltaUV1 = uv2 - uv1; glm::vec2 deltaUV2 = uv3 - uv1; 

掌握了必要的初始数据后,我们可以直接通过上一部分中的公式开始计算切线和双切线:

 float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y); tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x); tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y); tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z); tangent1 = glm::normalize(tangent1); bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x); bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y); bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z); bitangent1 = glm::normalize(bitangent1); [...] //         

首先,我们在单独的变量f中取出最终表达式的小数部分。 然后,对于向量的每个分量,我们执行矩阵乘法的相应部分,然后乘以f 。 将此代码与最终的计算公式进行比较,您可以看到这是其字面安排。 不要忘记最后进行归一化,以便找到的向量为单位。

由于三角形是平坦的图形,因此足以计算每个三角形的切线和双切线-所有顶点的它们都相同。 值得注意的是,使用模型的大多数实现(例如加载程序或景观生成器)都使用这种三角形的组织,它们与其他三角形共享顶点。 在这种情况下,开发人员通常会求助于普通顶点的平均参数,例如法线向量,切线和双切线,以获得更平滑的结果。 组成我们平面的三角形也共享多个顶点,但是由于它们都位于同一平面上,因此不需要求平均值。 尽管如此,记住这种方法在实际应用程序和任务中的存在还是很有用的。

所得切线和双切线向量必须分别具有值(1、0、0)和(0、1、0)。 它将与法线向量(0,0,1)一起形成正交矩阵TBN。 如果使用平面可视化结果基础,则会得到以下图像:


现在,有了计算的向量,您就可以继续完整地执行法线贴图。

切线空间中的法线贴图


首先,您需要在着色器中创建TBN矩阵。 为此,我们将通过顶点属性将预先准备的切向量和双切向量转移到顶点着色器:

 #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoords; layout (location = 3) in vec3 aTangent; layout (location = 4) in vec3 aBitangent; 

在顶点着色器代码本身中,我们直接形成矩阵:

 void main() { [...] vec3 T = normalize(vec3(model * vec4(aTangent, 0.0))); vec3 B = normalize(vec3(model * vec4(aBitangent, 0.0))); vec3 N = normalize(vec3(model * vec4(aNormal, 0.0))); mat3 TBN = mat3(T, B, N) } 

在上面的代码中,我们首先将切线空间基础上的所有向量转换为我们可以轻松使用的坐标系-在这种情况下,这是世界坐标系,然后将向量乘以模型矩阵模型 。 接下来,我们通过将所有三个对应的向量简单地传递给mat3类型的构造函数来创建TBN矩阵本身。 请注意,为了使顺序完全正确,有必要将向量不乘以模型矩阵,而是乘以法线矩阵,因为我们只对向量的方向感兴趣,而不对向量的位移或缩放感兴趣
严格来说,没有必要将双切向量转移到着色器。
由于TBN向量的三元组相互垂直,因此可以通过矢量乘法在着色器中找到双正切值:

  vec3 B = cross(N, T) 

那么,TBN矩阵被接收了,我们如何使用它呢? 实际上,在法线贴图中有两种使用方法:

  1. 使用TBN矩阵将所有必要的向量从切线转换为世界空间。 将结果传输到片段着色器,在这里也使用矩阵将向量从法线贴图转换为世界空间。 结果,法线向量将位于计算所有照明的空间中。
  2. 将逆矩阵取为TBN,并将所有必要的向量从世界空间转换为切线。 即 使用此矩阵将照明计算中涉及的矢量转换为切线空间。 在这种情况下,法向矢量在照明计算中也保持与其他参与者相同的空间。

让我们看一下第一个选项。 来自相应纹理的法线向量在切线空间中指定,而在照明计算中使用的其他向量在世界空间中定义。 通过将TBN矩阵传递到片段着色器,我们可以将通过从纹理切线到世界空间采样而获得的法向矢量转换,从而确保照明计算所有元素的坐标系统一。 在这种情况下,所有计算(尤其是标量向量乘法)都是正确的。

TBN矩阵的传输以最简单的方式完成:

 out VS_OUT { vec3 FragPos; vec2 TexCoords; mat3 TBN; } vs_out; void main() { [...] vs_out.TBN = mat3(T, B, N); } 

在片段着色器代码中,我们分别设置了mat3类型的输入变量:

 in VS_OUT { vec3 FragPos; vec2 TexCoords; mat3 TBN; } fs_in; 

有了矩阵,您可以指定从切线到世界空间的平移表达式来获取法线的代码:

 normal = texture(normalMap, fs_in.TexCoords).rgb; normal = normalize(normal * 2.0 - 1.0); normal = normalize(fs_in.TBN * normal); 

由于现在已将法线设置在世界空间中,因此无需更改着色器代码中的其他任何内容。 照明计算,因此假定以世界坐标给出的法向矢量。

让我们也看看第二种方法。它将需要获得逆TBN矩阵,并将照明计算中涉及的所有矢量从世界坐标系转移到与从纹理获得的法线矢量(切线)相对应的矢量。在这种情况下,TBN矩阵的形成保持不变,但是在传递给片段着色器之前,我们必须获得逆矩阵:

 vs_out.TBN = transpose(mat3(T, B, N)); 

请注意,使用了transpose()函数而不是inverse()这样的替换是正确的,因为对于正交矩阵(其中所有轴均由单位相互垂直的矢量表示),获得逆矩阵的结果与转置相同。这非常有用,因为在一般情况下,与转置相比,计算逆矩阵的计算量大得多。

在片段着色器代码中,我们将不转换法线向量,而是将其他重要向量从世界坐标系转换为切线,即lightDirviewDir该解决方案还将计算的所有元素都放入一个坐标系,这次是切线。

 void main() { vec3 normal = texture(normalMap, fs_in.TexCoords).rgb; normal = normalize(normal * 2.0 - 1.0); vec3 lightDir = fs_in.TBN * normalize(lightPos - fs_in.FragPos); vec3 viewDir = fs_in.TBN * normalize(viewPos - fs_in.FragPos); [...] } 

第二种方法似乎更耗时,并且在片段着色器中需要更多的矩阵乘法(这大大影响了性能)。为什么我们甚至开始拆卸它?
事实是,将向量从世界坐标转换为切线还提供了另一个优势:实际上,我们可以将整个转换代码从片段移动到顶点着色器!这种方法之所以有效是因为lightPosviewPos不会在片段之间变化,并且值是fs_in.FragPos我们还可以在顶点着色器中转换为切线空间,在片段着色器入口处的插值将是非常正确的。因此,对于第二种方法,不需要将所有这些向量转换为片段着色器代码中的切线空间,而第一种则需要它-因为法线对于每个片段都是唯一的。

结果,我们不再将矩阵逆从TBN传递到片段着色器,而是将其顶点,光源和观察者在切线空间中的位置矢量传递给它。因此,我们将摆脱片段着色器中昂贵的矩阵乘法,这将是一个重要的优化,因为顶点着色器的执行频率要低得多。正是这种优势使第二种方法在大多数情况下成为首选使用的类别。

 out VS_OUT { vec3 FragPos; vec2 TexCoords; vec3 TangentLightPos; vec3 TangentViewPos; vec3 TangentFragPos; } vs_out; uniform vec3 lightPos; uniform vec3 viewPos; [...] void main() { [...] mat3 TBN = transpose(mat3(T, B, N)); vs_out.TangentLightPos = TBN * lightPos; vs_out.TangentViewPos = TBN * viewPos; vs_out.TangentFragPos = TBN * vec3(model * vec4(aPos, 0.0)); 

在片段着色器中,我们切换到在切线空间的光照计算中使用新的输入变量。由于法线是在此空间中有条件定义的,因此所有计算都保持正确。
现在,所有法线贴图计算都在切线空间中执行,我们可以根据需要在应用程序中更改测试表面的方向,并且照明将保持正确:

 glm::mat4 model(1.0f); model = glm::rotate(model, (float)glfwGetTime() * -10.0f, glm::normalize(glm::vec3(1.0, 0.0, 1.0))); shader.setMat4("model", model); RenderQuad(); 

确实,从外观上看,一切看起来都应该如此:


资料在这里

复杂物体


因此,我们找出了如何在切线空间中执行法线映射,以及如何为此独立计算切线和偏置切线向量。幸运的是,这样的手动计算并不是一件经常的任务:在大多数情况下,此代码是由模型加载器中某些地方的开发人员实现的。在我们的情况下,这对于使用的Assimp加载程序来说是正确

加载模型时,Assimp提供了一个非常有用的选项标志:aiProcess_CalcTangentSpace当将其传递给ReadFile()函数时,库本身将为每个加载的顶点计算平滑切线和双切线-与此处讨论的过程类似。

 const aiScene *scene = importer.ReadFile( path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace ); 

之后,您可以直接访问计算出的切线:

 vector.x = mesh->mTangents[i].x; vector.y = mesh->mTangents[i].y; vector.z = mesh->mTangents[i].z; vertex.Tangent = vector; 

您还需要更新下载代码,以考虑收到纹理模型的法线贴图。 Wavefront Object(.obj)格式导出法线贴图,以便Assimp aiTextureType_NORMAL标志不能确保正确加载这些贴图,而一切都可以通过aiTextureType_HEIGHT标志正常工作。因此,就我个人而言,我通常通过以下方式加载法线贴图:

 vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal"); 

当然,此方法可能不适用于其他模型描述格式和文件类型。我还注意到,设置aiProcess_CalcTangentSpace标志并不总是有效。我们知道,切线的计算基于纹理坐标,但是,模型作者经常将各种技巧应用于纹理坐标,这破坏了切线的计算。因此,纹理坐标的镜像通常用于对称纹理模型。如果不考虑镜像的事实,则切线的计算将是不正确的。 Assimp不执行此会计。这里熟悉的纳米套装模型不适合演示,因为它也使用镜像。

但是,使用正确的纹理模型(使用法线和镜面贴图),测试应用程序将获得非常好的结果:


如您所见,法线映射的使用在细节上有明显的增加,并且在性能成本方面便宜。

不要忘记使用法线贴图可以帮助提高特定场景的性能。如果不使用它,只有通过增加多边形网格的密度才能实现模型细节。但是,此技术使您可以直观地实现低多边形网格的相同细节水平。您可以在下面看到这两种方法的比较:


使用法线贴图的高多边形模型和低多边形模型的详细程度实际上是无法区分的。因此,该技术是用几乎没有视觉质量损失的简化模型替换场景中的高多边形模型的好方法。

最后评论


还有一个有关法线贴图的技术细节,它可以在几乎不增加成本的情况下提高质量。

在为包含多个三角形的大量顶点的大型且复杂的网格计算切线的情况下,通常对切向量进行平均,以获得平滑且视觉上不错的法线贴图结果。但是,这会带来一个问题:求平均值后,三倍的TBN向量可能会失去相互的垂直性,这也意味着TBN矩阵的正交性会丢失。在一般情况下,基于非正交矩阵获得的法线映射结果只是略有错误,但我们仍然可以对其进行改进。

为此,只需应用简单的数学方法即可:Gram-Schmidt过程或我们三重TBN向量的重新正交化。在顶点着色器代码中:

 vec3 T = normalize(vec3(model * vec4(aTangent, 0.0))); vec3 N = normalize(vec3(model * vec4(aNormal, 0.0))); // - T  N T = normalize(T - dot(T, N) * N); //    B    T  N vec3 B = cross(N, T); mat3 TBN = mat3(T, B, N) 

尽管很小,但此修正却改善了法线贴图的质量,以换取微薄的开销。如果您对该步骤的细节感兴趣,可以观看法线贴图数学视频的最后一部分,下面提供了该链接。

其他资源



PS:我们有一个电报会议,以协调转账。如果您有强烈的帮助翻译的愿望,欢迎您!

Source: https://habr.com/ru/post/zh-CN415579/


All Articles