计算机图形学(OPENGL):法线贴图

如题所述

第1个回答  2022-06-18
本文同时发布在我的个人博客上: https://dragon_boy.gitee.io

  模型的网格构成其结构,纹理赋予其光影,但如果观察我们之前的所有例子,都会发现模型的表面都是扁平的,就算赋予了纹理也看起来不真实,因为现实生活中的大部分物体都是表面粗糙,凹凸不平的。
  比如,一个贴有砖块纹理的平面。砖墙本身应该表面凹凸不平,有缝隙,有划痕,有孔洞,运用我们之前学过的技术,我们会在之前的平面上贴上纹理来模拟砖墙:

  仔细观察的话,所有的凹凸不平、缝隙和孔洞这些细节全都没有表现出来,平面看上去非常扁平。我们其实可以利用一些技术来表现细节,比如使用高光贴图来让某些地方照亮的更少,但这不算是一种真正的解决方式。
  如果我们从灯光的角度去思考的话:平面是怎么样被渲染为完全扁平的平面的?我们可以想到是平面的法线,决定物体形状的就是它的法线。平面使用的是单独的法线,所以表面没有起伏变化,那么如果我们为每个片段计算不同的法线,并操作这些法线产生一些变化的话,表面应该就能看起来起伏了。下面的图说明了这个想法:

  为了使用法线贴图技术我们需要每个片段的法线。就像使用漫反射贴图和高光贴图,我们可以将每一片段的法线信息存储在一张纹理中。
  由于法线向量由几何方式存储,而纹理存储的是颜色信息,所以这二者的转换不那么直接。纹理中颜色由r、g、b三个组件构成向量表示,法线由x、y、z三个组件构成向量表示。法线的组件取值范围为[-1,1],颜色的范围是[0, 1],所以我们需要进行一下映射:

  这样的话,我们就可以将法线信息转化为颜色信息存储在纹理中。下面是砖块纹理的法线贴图:

  可以看到纹理图片大部分颜色与蓝色相关,这是因为大部分法线倾向于垂直于平面,也就是倾向于指向z轴,对应的是b通道,所以倾向于蓝色,依此类推解释其它的颜色。
  通过这个法线贴图,我们可以结合漫反射纹理来渲染一个平面。(记住,OpenGL纹理的原点在左下角,大部分图片的原点在左上角)我们需要做的就是按常规加载这张法线贴图,并设置相关参数。注意,在片元着色器中我们将使用法线贴图中的法线信息计算光照:

  加载法线贴图,并根据纹理坐标映射后,我们的到法线信息,首先将其标准化到[-1,1],接着照常计算光照。
  最后的结果就是这样:

  但存在一个限制法线贴图使用的问题,法线贴图中存储的法线信息大部分都是指向z轴的,如果平面也指向z轴,效果的确很好,但如果不是,比如下面的平面指向y轴,结果就不对了:

  另一种可行的方法是在不同的坐标空间中计算光照,来保证从法线贴图中采样的法线向量始终指向z轴,其它相关的向量也会转换到相同的坐标空间中。使用这种方法,我们可以复用一张法线贴图,而这种坐标空间被称为切线空间。

  在法线贴图中的法线向量在切线空间中表示,在这个空间中,法线基本都会大致指向+z轴的方向。切线空间是相对于每个平面三角形的空间,我们将这个空间作为法线贴图自己的空间,用来描述法线向量。当我们想要使用法线贴图中的法线进行计算时,我们就可以使用一个特殊的矩阵变换将法线从切线空间转化到世界空间或视图空间,这样就可以与物体相对应。
  所以解决上面法线贴图不正确的方法就是定义一个特殊的矩阵将切线空间中的法线进行一些转化,让法线大致指向+y轴。
  而这么一个特殊的矩阵被称为TBN矩阵,每个字母分别代表切线(Tangent)、双切线(Bitangent)、法线(Normal)向量,这三个向量将用来构成一个矩阵。为了获取这三个矩阵,我们定义切线空间的三个轴,上、右、前。
  我们已经有了代表上的轴向,即法线向量,右和前轴分别是切线和双切线向量,下面是图例说明:

  我们首先定义一个平面的四个顶点和每个顶点的纹理坐标(123和134两个三角形),以及平面的法线朝向:

  根据上面提到的步骤,计算第一个三角形的两条边和两条边对应的ΔUV坐标:

  然后,我们就可以按照等式计算切线和双切线了:

  作为结果,切线和双切线的值应该为(1,0,0)和(0,1,0),它们和法线(0,0,1)构成TBN矩阵,在平面上显示就是这样:

  我们首先在着色器中定义TBN矩阵,可以在顶点着色器中传入我们计算好的切线、双切线以及法线作为顶点属性:

  接着在main方法中创建TBN矩阵:

  我们首先将TBN三个向量分别转化到我们想要工作的空间中,然后组装为TBN矩阵。为了跟精确一些,我们可以将TBN三个向量分别进行和我们之前处理向量一样的操作,因为我们只关心这些向量的指向。
  使用这个TBN矩阵的方式有两种:

  在片元着色器中输入:

  使用TBN矩阵我们将采样的法线向量转化世界空间:

  接着介绍第二种,我们将TBN矩阵的逆输出到片元着色器:

  注意到我们使用的是转置,这是因为TBN矩阵是一个正交矩阵,它的逆等于它的转置,所以我们避免使用inverse来避免巨大的开销。
  接着在片元着色器中将所有与灯光计算相关的向量转化到切线空间:

  看起来第二种方式更为复杂,因为要计算的东西更多,但第二种方式有它的优点:我们可以将所有转化工作在顶点着色器中进行。这是可行的,因为lightPos和viewLPos这种向量并不会在片元着色器中更新,同时也可以在顶点着色器中计算fs_in.FragPos计算切线空间的位置。的确,考虑效率,不必在片元着色器中进行空间的转化。
  所以接下来我们在顶点着色器中完成这些操作:

  这样将相关变量传入片元着色器中就可以直接进行计算。
  为了观察光照是否正确,我们可以让平面一直旋转:

  最后的结果如下:

  针对复杂模型,我们并不经常手动计算切线空间的相关向量。比如在导入模型时,我们可以借助assimp库来帮助我们计算。
  assimp库有一个读文件方法有一个配置选项为aiProcess_CalTangentSpace,这样assimp可以为每个顶点都计算切线和双切线:

  我们可以像下面这样获取切线:

  这样的话,我们可以为模型加载它的法线贴图,我们使用aiTextureType_Height选项:

  针对复杂模型时,切线往往是经过许多顶点计算的,这样就可以得到一个平均值来获得平滑的结果,这样会造成一个问题,那就是T、B、N三个向量可能不会相互垂直了,也就是TBN矩阵不是正交的了。
  针对这一问题,我们可以使用格拉姆施密特方法来重新正交化TBN矩阵,在顶点着色器中这么做:

  最后,贴出原文地址供参考: https://learnopengl.com/Advanced-Lighting/Normal-Mapping
相似回答