当前位置: 首页 > news >正文

【OpenGL】LearnOpenGL学习笔记25 - 法线贴图 NormalMap

上接:https://blog.csdn.net/weixin_44506615/article/details/151876995?spm=1001.2014.3001.5501
完整代码:https://gitee.com/Duo1J/learn-open-gl | https://github.com/Duo1J/LearnOpenGL

法线贴图 (NormalMap)

换回之前的背包场景,观察之前绘制的背包可以发现,模型的表面非常的平坦
比如斧头的木把手应该是粗糙不平的,可现在看起来很光滑
法线贴图
如果以光线的视角来看这个问题可以发现,表面之所以看起来这么平坦,是模型表面完全平行规整的法线导致的,如下图所示 (图片来自于LearnOpenGL)
法线贴图
所以要做出粗糙不平的效果,我们可以对每个片段所对应的法线进行改变,将这种改变的xyz坐标存储在纹理中的rgb通道,这样的问题就叫 法线贴图 (Normal Map) 或者 凹凸贴图 (Bump Map)
法线贴图
这是一个砖墙的法线贴图,可以看见其大部分都是偏蓝色的,这是因为所有法线的指向都是偏向Z轴的 (0, 0, 1)

在之前的背包资源文件夹中有一张背包的法线贴图,但我们不能直接对法线贴图进行采样然后应用法线,这是因为我们的法向量是处于世界空间,它不总是Z轴垂直于表面,所以我们需要进行空间变换,这会引用一个新的空间:切线空间

切线空间

切线空间是位于三角形表面上的空间,其法线永远指向正Z方向,另外两个轴为T (切线 Tangent)B (副切线 Bitangent),加上 N (法线 Normal) 共称 TBN
TBN切线空间对于简单物体我们可以自行计算TBN,但对于较为复杂的物体计算较为麻烦,之前加载模型使用的Assimp库可以帮我们来计算切线,接下来修改一下之前的Model、Mesh、Vertex、Texture类
Vertex.h
增加切线和副切线,由于后续绑定顶点时需要连续的结构,这里需要注意顺序

#pragma once#include <glm.hpp>/**
* 顶点
*/
struct Vertex
{/*** 位置*/glm::vec3 position;/*** 法向量*/glm::vec3 normal;/*** UV*/glm::vec2 texCoord;/*** 切线*/glm::vec3 tangent;/*** 副切线*/glm::vec3 bitangent;
};

Model.cpp

void Model::LoadModel(std::string path)
{// ...// aiProcess_CalcTangentSpace: 计算切线空间const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);// ...
}Mesh Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
{// ...// 切线、副切线if (mesh->HasTangentsAndBitangents()){vertex.tangent = glm::vec3(mesh->mTangents[i].x, mesh->mTangents[i].y, mesh->mTangents[i].z);vertex.bitangent = glm::vec3(mesh->mBitangents[i].x, mesh->mBitangents[i].y, mesh->mBitangents[i].z);}// ...// 加载材质和纹理if (mesh->mMaterialIndex >= 0){// ...// 加载法线贴图std::vector<Texture> normalMaps = LoadMaterialTextures(material, aiTextureType_HEIGHT, TextureType::NORMAL);textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());}
}

Mesh.cpp

void Mesh::Draw(const Shader& shader, int instanceCnt)
{// ...unsigned int normalNum = 0;// ...else if (texture.type == TextureType::NORMAL){number = std::to_string(++normalNum);}
}void Mesh::CreateBuffer()
{// ...// 外部临时绑定了Instance Offset,先跳过3glEnableVertexAttribArray(4);glVertexAttribPointer(4, 3, GL_FLOAT, false, sizeof(Vertex), (void*)offsetof(Vertex, tangent));glEnableVertexAttribArray(5);glVertexAttribPointer(5, 3, GL_FLOAT, false, sizeof(Vertex), (void*)offsetof(Vertex, bitangent));
}

Texture.cpp

std::string Texture::GetTypeName()
{if (type == TextureType::DIFFUSE){return "tex_diffuse";}else if (type == TextureType::SPECULAR){return "tex_specular";}else if (type == TextureType::NORMAL){return "tex_normal";}else{assert(false);return "";}
}

切线空间准备完毕,接下来可以开始应用法线贴图了

应用法线贴图

对于切线空间的转换有两种路径,先来尝试第一种,将采样后的法线左乘TBN矩阵转换到世界空间来使用
VertexShader.glsl

#version 330 corelayout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in vec3 offset;
// 切线
layout (location = 4) in vec3 aTangent;
// 副切线
layout (location = 5) in vec3 aBitangent;out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;uniform mat4 model;
layout (std140) uniform Matrices
{mat4 view;mat4 projection;
};out VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;// 输出TBN矩阵mat3 TBN;vec3 Debug;
} vs_out;void main()
{gl_Position = projection * view * model * vec4(aPos + offset, 1.0);FragPos = vec3(model * vec4(aPos + offset, 1.0));vs_out.FragPos = FragPos;Normal = mat3(transpose(inverse(model))) * aNormal;vs_out.Normal = Normal;TexCoords = aTexCoords;vs_out.TexCoords = aTexCoords;// 计算TBN矩阵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)));vs_out.TBN = mat3(T, B, N);
}

FragmentShader.glsl

// ...
struct Material {sampler2D tex_diffuse1;sampler2D tex_diffuse2;// ...sampler2D tex_specular1;sampler2D tex_specular2;// ...// 增加法线贴图sampler2D tex_normal1;sampler2D tex_normal2;// ...float shininess;
};
// ...
in VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;mat3 TBN;vec3 Debug;
} fs_in;
// ...
void main()
{// 采样法线贴图vec3 normal = texture(material.tex_normal1, fs_in.TexCoords).rgb;// [-1, 1]转换到[0, 1]normal = normalize(normal * 2 - 1);// 左乘TBN矩阵转换到世界空间normal = normalize(fs_in.TBN * normal);// ...
}

编译运行,可以发现木头把手有了凹凸不平的粗糙感
法线贴图
第二种方式则是反过来,我们使用TBN的逆矩阵将所有相关的世界空间向量转换为切线空间向量
VertexShader.glsl

// ...
out VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;mat3 TBN;vec3 TangentViewPos;vec3 TangentFragPos;vec3 Debug;
} vs_out;
// ...
void main()
{// ...// 计算TBN矩阵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)));// 这里不使用inverse是因为它是一个正交矩阵// 正交矩阵的逆矩阵和转置矩阵相等,但是transpose效率更高mat3 TBN = transpose(mat3(T, B, N));// 还是将TBN交给片段着色器用来计算lightPos,就先不移到顶点着色器了vs_out.TBN = TBN;vs_out.TangentViewPos  = TBN * viewPos;vs_out.TangentFragPos  = TBN * vec3(model * vec4(aPos + offset, 0.0));
}

FragmentShader.glsl
将用到的ViewPos、FragPos以及LightPos转换为切线空间向量

// 光照函数中的light.position改为
// normalize(fs_in.TBN * light.position)void main()
{// 采样法线贴图vec3 normal = texture(material.tex_normal1, fs_in.TexCoords).rgb;// [-1, 1]转换到[0, 1]normal = normalize(normal * 2 - 1);// 换为切线空间vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);// 平行光vec3 result = CalcDirectionalLight(dirLight, normal, viewDir);// 点光源for (int i = 0; i < POINT_LIGHT_NUM; i++){result += CalcPointLight(pointLight[i], normal, viewDir, fs_in.TangentFragPos);}FragColor = vec4(result, 1.0);
}

编译运行,我们可以看见和第一种方法一样的效果
但是这样我们将计算挪到了顶点着色器 (LightPos的转换也可以提前到顶点着色器进行),而通常顶点着色器运行次数会小于片段着色器,这会带来性能的提升

此外,还有一个问题和对应的技巧

当在更大的网格上计算切线向量的时候,它们往往有很大数量的共享顶点,当法向贴图应用到这些表面时将切线向量平均化通常能获得更好更平滑的结果。这样做有个问题,就是TBN向量可能会不能互相垂直,这意味着TBN矩阵不再是正交矩阵了。法线贴图可能会稍稍偏移。

我们可以使用叫做格拉姆-施密特正交化过程(Gram-Schmidt process)的数学技巧,我们可以对TBN向量进行重正交化,这样每个向量就又会重新垂直了。在顶点着色器中我们这样做:

vec3 T = normalize(vec3(model * vec4(tangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(normal, 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(T, N);mat3 TBN = mat3(T, B, N)

完整代码可在顶部Git仓库中找到

http://www.dtcms.com/a/392927.html

相关文章:

  • UE5 基础应用 —— 09 - 行为树 简单使用
  • 客户端实现信道管理
  • 异常解决记录 | Yarn NodeManager 注册异常
  • 【C#】C# 调用 Python 脚本正确姿势:解决 WaitForExit 死锁与退出检测问题
  • Java25新特性
  • 卷积神经网络CNN-part9-DenseNet
  • 深入浅出密码学第一章课后题(持续更新)
  • Mysql 入门概览
  • 大模型中权重共享的作用?
  • 【精品资料鉴赏】55页可编辑PPT详解 数字化高校智慧后勤解决方案
  • LLM大模型 - 实战篇 - AI Agents的开发应用
  • 【分布式技术】RedisShake相关功能详细介绍
  • qsv:一款高性能的CSV数据处理工具
  • `html` 将视频作为背景
  • 口播提词器怎么选?手机提词器实测指南与参数推荐
  • 解剖线性表
  • 计算数学研究方向有哪些细分领域?
  • [xboard]08-Makefile逐行分析2
  • Clash 中 REJECT 的技术原理与解决方案 —— 以哔哩哔哩延迟问题为例
  • 庖丁解牛与专家思维:道家的“心手合一”训练法
  • matlab通过GUI实现点云的读取、自定义显示和保存
  • 工业现场实战:如何利用智能网关实现西门子PLC与库卡机器人的无缝连接
  • 【开题答辩全过程】以 Java程序设计课程作业数据分析为例,包含答辩的问题和答案
  • ubuntu配置cuda与torch
  • C语言:输出水仙花数
  • 进程的创建
  • 如何用Anaconda Navigator和命令行管理Python库?
  • 28 种 LLM 越狱攻击全景拆解(2025.9 版)从“AIM”到“Generation Exploitation”,一张防御地图看懂所有套路
  • 第14章 智能床位
  • 总结一下MySQL数据库服务器性能优化的几个维度