我们通过在这些平面三角形上包裹 2D 纹理来增强真实感,隐藏多边形只是很小的平面三角形的事实。

从照明技术的角度来看,确定对象形状的唯一方法是通过其垂直法向量

这种使用每片段法线与每表面法线相比的技术称为法线贴图或凹凸贴图。 应用于砖平面它看起来有点像这样:

 如您所见,它以相对较低的成本在细节方面提供了巨大的提升。 由于我们只更改每个片段的法线向量,因此无需更改照明方程。

基础知识:

1.Normal mapping(法线贴图)

虽然法向量是几何实体,纹理通常只用于颜色信息, 颜色向量表示为具有 rgb 分量的 3D 向量。 我们可以类似地将法向量的 x、y 和 z 分量存储在相应的颜色分量中。 法向量范围在 -1 和 1 之间,因此它们首先映射到 [0,1]:

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

通过将法线向量转换为这样的 RGB 颜色分量,我们可以将源自表面形状的每个片段法线存储到 2D 纹理上。

这(以及您在网上找到的几乎所有法线贴图)都会带有蓝色调。 这是因为法线都紧密地向外指向正 z 轴 (0,0,1):偏蓝的颜色。 颜色偏差表示法向量与一般正 z 方向略有偏移,从而为纹理提供深度感。 例如,您可以看到在每块砖的顶部,朝向上下的法线颜色往往更偏绿,这是有道理的,因为砖的顶部的法线会更多地指向正 y 方向 (0,1,0),这会发生 成为绿色!

加载两个纹理,将它们绑定到适当的纹理单元,并在光照片段着色器中使用以下更改渲染平面:

uniform sampler2D normalMap;  void main()
{           // obtain normal from normal map in range [0,1]normal = texture(normalMap, fs_in.TexCoords).rgb;// transform normal vector to range [-1,1]normal = normalize(normal * 2.0 - 1.0);   [...]// proceed with lighting as normal
}  

通过随着时间的推移缓慢移动光源,您可以使用法线贴图真正获得深度感。 运行这个法线贴图示例给出了本章开头所示的确切结果:

我们使用的法线贴图具有法线向量,他们都指向一个固定的方向。但是,如果我们换个角度的贴图,使用这个法线向量,同样会指向那个固定的方向,导致法线贴图出问题了。

存在一种解决方案,法线贴图向量始终指向正 z 方向的坐标空间; 然后所有其他光照向量都相对于这个正 z 方向进行转换。 这样我们就可以始终使用相同的法线贴图,无论方向如何。 这个坐标空间称为tangent space切线空间

2.Tangent space(切线空间)

法线贴图是在切线空间中定义的,解决问题的一种方法是计算一个矩阵,将法线从切线空间转换到不同的空间,使它们与表面的法线方向对齐:然后法线向量都指向大致在正 y 方向。切线空间的伟大之处在于我们可以为任何类型的表面计算这个矩阵,以便我们可以正确地将切线空间的 z 方向与表面的法线方向对齐。

这样的矩阵称为 TBN (Tangent, Bitangent and Normal vector.)矩阵,其中字母表示切线、双切线和法线向量。 这些是我们需要构建这个矩阵的向量。 为了构造这样一个改变基矩阵,将切线空间向量转换到不同的坐标空间,我们需要三个沿法线贴图表面对齐的垂直向量:向上、向右和向前向量;

我们已经知道向上向量,它是表面的法向量。 右向量和前向量分别是tangent切线bitangent vector双切线向量。

计算切线和双切线向量并不像法线向量那么简单。 我们可以从图像中看到法线贴图的tangent切线bitangent vector双切线向量的方向与我们定义表面纹理坐标的方向对齐。 我们将使用这个事实来计算每个表面的切线和双切线向量。

从图中我们可以看出三角形的边E2的纹理坐标差(记为ΔU2和ΔV2)与切向量T双切向量B在同一方向上表示。因此我们可以写出两条显示的边 三角形的 E1 和 E2 作为切向量 T 和双切向量 B 的线性组合:

T:tangent vector切向量

B:bitangent vector双切线向量

DV:三角形边E的竖分量

DU:三角形边E的横分量

也可以写成:把每个字母X,都拆成了(X1x,X2x,X3x)

我们可以将 E1,E2分别看作为三角形的两个边向量,将 ΔU 和 ΔV 计算为它们的纹理坐标差。 然后我们剩下两个未知数(切线 T 和双切线 B)和两个方程。 您可能从代数课中记得,这使我们能够求解 T 和 B。

最后一个方程允许我们用不同的形式来写:矩阵乘法:

 转换一下:

再转换一下:

最后一个方程为我们提供了一个公式,用于根据三角形的两条边及其纹理坐标计算切向量 T双切向量 B

我们可以从三角形的顶点及其纹理坐标(因为纹理坐标与切线向量在同一空间中)计算切线和双切线

3.Manual calculation of tangents and bitangents (手动计算切线和副切线)

让我们假设平面是由以下向量构成的(pos1, pos2,pos3 和 pos1, pos3, pos4 作为它的两个三角形):

// positions
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);
// texture coordinates
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);
// normal vector
glm::vec3 nm(0.0, 0.0, 1.0);  

我们首先计算第一个三角形的边和 delta UV 坐标:

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);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);[...] // similar procedure for calculating tangent/bitangent for plane's second triangle

在平面上可视化,TBN 向量将如下所示:

通过为每个顶点定义切线和双切线向量,我们可以开始实现正确的法线贴图。

4.Tangent space normal mapping(切线空间法线贴图)

为了使法线贴图工作,我们首先必须在着色器中创建一个 TBN(tangent和bitangent和normal) 矩阵。 为此,我们将先前计算的切线和双切线向量作为顶点属性传递给顶点着色器:

#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;  

创建TBN矩阵:

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 向量与法线矩阵相乘,因为我们只关心向量的方向。

从技术上讲,顶点着色器中不需要双切线变量。 所有三个 TBN 向量都相互垂直,因此我们可以通过 T 和 N 向量的叉积在顶点着色器中自己计算双切线: vec3 B = cross(N, T);

有两种方法可以使用 TBN 矩阵(里面有法线坐标系的向量)进行法线贴图:

1.我们对任何向量都把 TBN 矩阵切线坐标系转换到世界空间坐标系,将其提供给片段着色器,并把 TBN 矩阵采样的法线从切线空间转换到世界空间; 然后法线与其他照明变量位于同一空间中。

2.我们对任何向量都通过TBN 矩阵的逆矩阵世界空间转换到切线空间的,并使用该矩阵将其他相关照明变量转换到切线空间而不是法线; 然后法线再次与其他照明变量位于同一空间中。

通过将 TBN 矩阵传递给片段着色器,我们可以将采样的切线空间法线与该 TBN 矩阵相乘,以将法线向量转换为与其他照明向量相同的参考空间。

总结:3个ts点,能渲染出一片区域,无数个fs点,而第二种,fs中直接用切线空间的数据进行计算,省去了很多矩阵运算,无数个fs点都省去了矩阵运算。具体怎么省去的,可以看后面的fs代码

发送TBN矩阵给fs:

vs:


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

fs:

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

有了这个 TBN 矩阵,我们现在可以更新法线映射代码以包含切线到世界空间的变换:

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

由于生成的法线现在位于世界空间中,因此无需更改任何其他片段着色器代码,因为照明代码假定法线向量位于世界空间中。

第二种情况:

让我们再回顾一下第二种情况,我们取 TBN 矩阵的逆矩阵,将所有相关的世界空间向量变换到采样法向量所在的空间:切线空间。 TBN 矩阵的构造保持不变,但我们在将其发送到片段着色器之前先反转矩阵

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

请注意,我们在这里使用转置函数而不是反函数。 正交矩阵的一个重要特性(每个轴都是一个垂直的单位向量)是正交矩阵的转置等于它的逆矩阵。 这是一个很好的属性,因为逆是昂贵的,而转置不是

在片段着色器中,我们不转换法向量,但我们将其他相关向量转换到切线空间,即 lightDir 和 viewDir 向量。 这样,每个向量都在同一个坐标空间中:切线空间。

vs:

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);    [...]
}  

第二种方法看起来工作量更大,而且还需要在片段着色器中进行矩阵乘法,那么我们为什么要为第二种方法烦恼呢?

嗯,将向量从世界空间转换到切线空间有一个额外的优势,因为我们可以在顶点着色器而不是片段着色器中将所有相关的照明向量转换到切线空间。这是有效的,因为 lightPos 和 viewPos 不会在每个fragment shader上运行,对于 fs_in.FragPos 我们可以计算它在顶点着色器中的切线空间位置并让片段差值完成它的工作。实际上不需要将向量转换到片段着色器中所需要的切线空间,而第一种方法则是必要的,因为采样的法线向量特定于每个片段着色器运行

因此,我们不是将 TBN 矩阵的逆发送到片段着色器,而是将切线空间光位置、视图位置和顶点位置发送到片段着色器。这使我们不必在片段着色器中进行矩阵乘法。这是一个很好的优化,因为顶点着色器的运行频率远低于片段着色器。这也是为什么这种方法通常是首选方法的原因。

总结:第一种,每次vs传给fs的切线TBN向量都不同,有可能1个vs点,重复计算10遍fs每次值都不同,而用第二种,1个vs点,可以传给10个fs一样的数,所以不用每次都计算?

vs: 

out VS_OUT {vec3 FragPos;vec2 TexCoords;vec3 TangentLightPos;vec3 TangentViewPos;vec3 TangentFragPos;
} vs_out;uniform vec3 lightPos;
uniform vec3 viewPos;[...]void main()
{    [...]//反转TBN,则可以达到,点由世界坐标系转换到法线坐标系中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, 1.0));
}  

在片段着色器中,我们然后使用这些新的输入变量来计算切线空间中的照明。 由于法向量已经在切线空间中,所以光照是有意义的。

fs:

#version 330 core
out vec4 FragColor;in VS_OUT {vec3 FragPos;vec2 TexCoords;vec3 TangentLightPos;vec3 TangentViewPos;vec3 TangentFragPos;
} fs_in;uniform sampler2D diffuseMap;
uniform sampler2D normalMap;uniform vec3 lightPos;
uniform vec3 viewPos;void main()
{           // obtain normal from normal map in range [0,1]vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;// transform normal vector to range [-1,1]normal = normalize(normal * 2.0 - 1.0);  // this normal is in tangent space// get diffuse colorvec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;// ambientvec3 ambient = 0.1 * color;// diffuse   这里的计算,直接用法线坐标系的东西计算了,因为法线坐标系的差值,和现实坐标系的差值是一样的vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);float diff = max(dot(lightDir, normal), 0.0);vec3 diffuse = diff * color;// specularvec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);vec3 reflectDir = reflect(-lightDir, normal);vec3 halfwayDir = normalize(lightDir + viewDir);  float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);vec3 specular = vec3(0.2) * spec;FragColor = vec4(ambient + diffuse + specular, 1.0);
}

5.Complex objects(复杂物体,非重点)

Assimp 有一个非常有用的配置位,我们可以在加载名为 aiProcess_CalcTangentSpace 的模型时进行设置。 当 aiProcess_CalcTangentSpace 位被提供给 Assimp 的 ReadFile 函数时,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> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");  

使用法线贴图,我们可以使用更少的顶点在网格上获得相同级别的细节。 下面来自 Paolo Cignoni 的图片显示了两种方法的很好比较:

是用低顶点网格替换高顶点网格而不会丢失(太多)细节的好工具。

当在共享大量顶点的较大网格上计算切线向量时,切线向量通常被平均以提供良好且平滑的结果。 这种方法的一个问题是三个 TBN 向量可能最终不垂直,这意味着最终的 TBN 矩阵将不再是正交的。 使用非正交 TBN 矩阵时,法线贴图只会略微偏离,但这仍然是我们可以改进的地方。

使用称为 Gram-Schmidt 过程的数学技巧,我们可以重新正交化 TBN 向量,使每个向量再次垂直于其他向量。 在顶点着色器中,我们会这样做:

vec3 T = normalize(vec3(model * vec4(aTangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(aNormal, 0.0)));
// re-orthogonalize T with respect to N
T = normalize(T - dot(T, N) * N);
// then retrieve perpendicular vector B with the cross product of T and N
vec3 B = cross(N, T);mat3 TBN = mat3(T, B, N)  

这虽然稍微提高了一点点,但通常会以一点额外的成本改善法线贴图结果。

Learn OpenGL 笔记6.5 Normal Mapping(法线贴图)相关推荐

  1. Normal map (Bump mapping) 法线贴图(凹凸映射) Standard Shader系列10

    Normal map (Bump mapping) 法线贴图(凹凸映射) 本文档主要是对Unity官方手册的个人理解与总结(其实以翻译记录为主:>) 仅作为个人学习使用,不得作为商业用途,欢迎转 ...

  2. Real-Time Rendering——6.7.1 Blinn’s Methods 6.7.2 Normal Mapping法线映射

    6.7.1 Blinn's Methods Blinn's original bump mapping method stores two signed values, bu and bv, at e ...

  3. OpenGL.Shader:9-学习光照-法线贴图(计算TBN矩阵)

    OpenGL.Shader:9-学习光照-法线贴图(计算TBN矩阵) 这次文章学习法线贴图,法线贴图在游戏开发和GIS系统开发当中尤为广泛,其表现力特别的强,绘制的效果特别接近真实.更重要的一点就是, ...

  4. Learn OpenGL 笔记7.1 PBR Theory(physically based rendering基于物理的渲染 理论)

    PBR,或更通常称为基于物理的渲染,是一组渲染技术,它们或多或少基于与物理世界更接近的相同基础理论.由于基于物理的渲染旨在以物理上合理的方式模拟光线,因此与我们的原始光照算法(如 Phong 和 Bl ...

  5. Learn OpenGL 笔记6.9 Deferred Shading(延迟着色)

    到目前为止,我们进行照明的方式称为forward rendering前向渲染或forward shading前向着色.我们渲染对象,根据场景中的所有光源对其进行照明.我们为场景中的每个对象分别为每个对 ...

  6. Learn OpenGL 笔记6.10 SSAO(Screen Space Ambient Occlusion屏幕空间环境光遮蔽)

    我们在基本照明一章中简要介绍了该主题:ambient lighting环境光. Ambient lighting环境光是一个固定的光常数,我们添加到场景的整体照明中以模拟光的scattering散射. ...

  7. ShaderLab自学笔记(1):法线贴图和法线空间

    法线贴图和法线空间 为啥要有切线空间和啥是切线空间 纹理坐标跟位置坐标是啥关系 切线坐标系的求法 1.为啥要有切线空间和啥是切线空间 为啥要有切线空间? 3D空间中不同的坐标系有不用的用处.如局部空间 ...

  8. Learn OpenGL 笔记7.3 PBR-IBL-Diffuse irradiance(Image based lighting-漫反射辐照度)

    IBL,或image based lighting基于图像的照明,是一组照明对象的技术,不是像前一章那样通过直接分析光,而是将周围环境视为一个 big light source大光源.这通常通过操纵立 ...

  9. Learn OpenGL 笔记7.4 PBR-Specular IBL(Image based lighting-特殊的基于图像的照明)

    在上一章中,我们通过预先计算辐照度贴图作为照明的间接漫反射部分,将 PBR 与基于图像的照明相结合. 在本章中,我们将关注反射方程的specular part镜面反射部分: 您会注意到 Cook-To ...

最新文章

  1. poj2602(高精度模拟加法)
  2. linux 运维高级脚本生成器,Linux运维系列,Shell高级脚本自动化编程实战
  3. SAP Commerce Cloud 产品主数据读取的单步调试
  4. 代码Review发现问题
  5. cnpm不是内部或外部命令 cnpm: command not found 解决方案 cnpm
  6. 真正厉害的产品经理,都是“数据思维”的高手
  7. 虚拟机网络无法连接问题解决(超简单)
  8. 米斯特白帽培训讲义(v2)漏洞篇 第三方风险
  9. Redis info信息(转载)
  10. Mac使用NDK编译FFmpeg4.0.2单独so库(验证可用)
  11. EXCEL技能1:SUMIFS 小白详解
  12. 【镜像取证篇】常见镜像文件类型
  13. StatQuest系列之t-SNE
  14. facebook最全面中文介绍,让你更加了解facebook
  15. mysql中删除数据库语句
  16. 【Python】潜水小白,分享一个简单基础的tkinter的猜拳小游戏
  17. 学生交作业,现在都流行用二维码了
  18. mysql timestamp 比较_解析mysql TIMESTAMP(时间戳)和datetime不同之处比较
  19. oracle join 优化询,oracle中优化left join的工作心得
  20. 【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解

热门文章

  1. 小爱音箱 电脑 麦克风_开箱,小米小爱音箱万能遥控版,这样的操作你知道吗?...
  2. 12306 抢票软件使用记录
  3. 如何能成为一名真正电子工程师【转】
  4. miflash 刷机超过1000s还未完成
  5. 无线WiFi网络的密码破解攻防及原理详解
  6. IDEA的校园邮箱激活方式
  7. 计算机主板风扇安装,电脑cpu风扇怎么拆,cpu风扇安装,如何拆cpu风扇-中关村在线...
  8. java技术最吸引的点_简单几步让演示文稿更有吸引力
  9. Apache MINA框架快速入门
  10. 牛客OI周赛7-提高组(B 小睿睿的询问)