• OpenGL - Normal Mapping


    以光照算法的视角考虑的话,只有一件事决定物体的形状,这就是垂直于它的法线向量。砖块表面只有一个法线向量,表面完全根据这个法线向量被以一致的方式照亮。如果每个fragment都是用自己的不同的法线会怎样?这样我们就可以根据表面细微的细节对法线向量进行改变,这样就会获得一种表面看起来要复杂得多的幻觉:
    在这里插入图片描述
    每个fragment使用了自己的法线,我们就可以让光照相信一个表面由很多微小的(垂直于法线向量的)平面所组成,物体表面的细节将会得到极大提升。这种每个fragment使用各自的法线,替代一个面上所有fragment使用同一个法线的技术叫做法线贴图(normal mapping)凹凸贴图(bump mapping)

    diffuse贴图和specular贴图一样,我们可以使用一个2D纹理来储存法线数据。2D纹理不仅可以储存颜色和光照数据,还可以储存法线向量。将法线向量的x、y、z元素储存到纹理中,代替颜色的r、g、b元素。法线向量的范围在-1~1之间,所以我们先要将其映射到0~1的范围:

    vec3 rgb_normal = normal * 0.5 + 0.5; // 从 [-1,1] 转换至 [0,1]
    
    • 1

    将法线向量变换为RGB颜色元素,我们就能把根据表面的形状的fragment的法线保存在2D纹理中。
    在这里插入图片描述
    所有法线的指向都偏向z轴(0, 0, 1),这是一种偏蓝的颜色。法线向量从z轴方向也向其他方向轻微偏移,颜色也就发生了轻微变化,这样看起来便有了一种深度。例如,你可以看到在每个砖块的顶部,颜色倾向于偏绿,这是因为砖块的顶部的法线偏向于指向正y轴(0, 1, 0),这样它就是绿色的了。

    如果我们在表面法线指向正y方向的平面上使用同一个法线贴图会发生什么?

    在这里插入图片描述
    平面的表面法线现在指向了y,而采样得到的法线仍然指向的是z。结果就是光照仍然认为表面法线和之前朝向正z方向时一样,这样光照就不对了。

    在一个不同的坐标空间中进行光照,这个坐标空间里,法线贴图向量总是指向这个坐标空间的正z方向;所有的光照向量都相对与这个正z方向进行变换。这样我们就能始终使用同样的法线贴图,不管朝向问题。这个坐标空间叫做切线空间(tangent space)。切线空间的一大好处是我们可以为任何类型的表面计算出一个矩阵,把法线从切线空间变换到一个不同的空间,这样它们就能和表面法线方向对齐。

    这种矩阵叫做TBN矩阵,这三个字母分别代表tangentbitangentnormal向量

    在这里插入图片描述
    有了TBN矩阵,如果来使用它呢?通常来说有两种方式使用它:

    1. 我们直接使用TBN矩阵,这个矩阵可以把切线坐标空间的向量转换到世界坐标空间。因此我们把它传给片段着色器中,把通过采样得到的法线坐标左乘上TBN矩阵,转换到世界坐标空间中,这样所有法线和其他光照变量就在同一个坐标系中了。
    2. 我们也可以使用TBN矩阵的逆矩阵,这个矩阵可以把世界坐标空间的向量转换到切线坐标空间。因此我们使用这个矩阵左乘其他光照变量,把他们转换到切线空间,这样法线和其他光照变量再一次在一个坐标系中了。

    主要利用第二种方式,将向量从世界空间转换到切线空间有个额外好处,我们可以把所有相关向量在顶点着色器中转换到切线空间,不用在像素着色器中做这件事,而且顶点着色器通常比像素着色器运行的少,会节省性能。

    // 发送给像素着色器之前先要求逆矩阵:
    // 正交矩阵(每个轴既是单位向量同时相互垂直)的一大属性是一个正交矩阵的置换矩阵与它的逆矩阵相等。这个属性很重要因为逆矩阵的求得比求置换开销大, 结果却是一样的。
    vs_out.TBN = transpose(mat3(T, B, N));
    
    • 1
    • 2
    • 3

    Assimp有个很有用的配置,在我们加载模型的时候调用aiProcess_CalcTangentSpace。当aiProcess_CalcTangentSpace应用到AssimpReadFile函数时,Assimp会为每个加载的顶点计算出柔和的切线和副切线向量:

    const aiScene* scene = importer.ReadFile(
        path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace
    );
    
    // 通过下面的代码用Assimp获取计算出来的切线空间:
    vector.x = mesh->mTangents[i].x;
    vector.y = mesh->mTangents[i].y;
    vector.z = mesh->mTangents[i].z;
    vertex.Tangent = vector;
    
    // 更新模型加载器,用以从带纹理模型中加载法线贴图。
    vector<Texture> specularMaps = this->loadMaterialTextures(
        material, aiTextureType_HEIGHT, "texture_normal"
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用法线贴图也是一种提升你的场景的表现的重要方式。在使用法线贴图之前你不得不使用相当多的顶点才能表现出一个更精细的网格,但使用了法线贴图我们可以使用更少的顶点表现出同样丰富的细节:
    在这里插入图片描述
    高精度网格和使用法线贴图的低精度网格几乎区分不出来。所以法线贴图不仅看起来漂亮,它也是一个将高精度多边形转换为低精度多边形而不失细节的重要工具。

  • 相关阅读:
    【Dive into Deep Learning / 动手学深度学习】第十章 - 第二节:注意力汇聚:Nadaraya-Watson 核回归
    操作教程丨MaxKB+Ollama:快速构建基于大语言模型的本地知识库问答系统
    【广州华锐互动】VR高层小区安全疏散演练系统
    Java--多线程
    vue前端实现下载文件功能
    MCU与MPU的区别
    深度学习Course5第四周Transformers习题整理
    接口测试框架:Http+Requests(1)
    iview的表格实现单元格行编辑功能
    1488. 避免洪水泛滥
  • 原文地址:https://blog.csdn.net/qq_42403042/article/details/126576304