文在Blinn-phong光照模型基础上添加法线贴图。

看案例的时候总感觉很简单,但是自己写了之后发现还是有很多细节要注意的。

1.法线贴图

1.1 基本原理

借助一张纹理贴图颜色值(RGB)存储物体的表面法线方向,比如下面这张图,大部分是蓝色RGB:(0,0,1),对应了法线方向(0,0,1).

但是法线范围应该是【-1,1】,表示正反两个方向,所以要进行转化。

适合少顶点的情况下增加真实感,在多顶点情况下对比不是很大。

1.2 基本实现

仍然是基于原来的漫反射贴图计算光照,但是在漫反射和高光反射的计算上会更加细致,因为两者都与法线相关。

漫反射:

高光反射:

Blinn-phong光照计算公式:

2. TBN矩阵

2.1 基本原理

一开始想一个向量如何转换到另一个坐标,就是进行平移然后旋转(不考虑缩放)。

平移的话直到两个原点就行了,但是旋转还需要知道沿着三个向量的旋转角度。所以并不好求。

引入TBN矩阵

给定三个相互垂直的基向量确定一个坐标系,坐标其实就是该点在三个基向量上的投影大小,也就是点乘。

也就是说如果想将一个坐标P从A坐标系变换到B坐标系,知道三个相互垂直的基向量(A坐标系),将这三个基向量用B坐标系表示即可,然后对点P分别对三个向量求点乘即可(得到投影大小)。

T、B、N是行向量表示的

:正交矩阵(每个轴既是单位向量同时相互垂直)的置换矩阵与它的逆矩阵相等。这个属性很重要因为逆矩阵的求得比求置换开销大;结果却是一样的。

2.2 求TBN矩阵

因为已经直到原来的法线,求TBN矩阵其实只需要求其中一个切线即可,因为副切线可以用切线和法线的叉乘求到。

对于简单的平面

我们可以手工计算切线。

这里的计算都是在切线空间下计算的(后续再转化到世界空间里),默认T和B是沿着xy方向。

两条线缺点一个平面,将两个方向向E1、E2用基函数进行表示,可以得到下面的式子:

矩阵表示如下:

两边左乘UV矩阵的逆矩阵,结果最终可以表示为:

对于复杂模型

当我们需要对复杂多面体进行计算时,就需要为每个加载的顶点计算出柔和的切线和副切线向量,这明显是很庞大的工作量。因为每个顶点对应的切线空间可能都是不同的。

好在我们不需要手工计算每个顶点对应的切线和副切线,assimp库(Open Asset Import Library)在加载模型时候帮我们计算好了每个顶点的信息。我们只需要读取,然后放入缓存中传递到着色器程序。

2.3 格拉姆-施密特正交化(Gram-Schmidt process)

原因:求得TBN三个基函数后,其实这三个基向量不一定是两两垂直的。这意味着TBN矩阵不再是正交矩阵了,法线贴图可能会稍稍偏移。

首先副切线B是用切线和法线的叉乘求到,所以B和T、N是相互垂直的。

所以对T或者N,需要进行正交化,这里对T进行修正,简单理解如下:T' + N(NT)=T,求出T'然后单位化即可。

3. 代码实现

3.1 传递数据到着色器

这里可以用LearnOpenGL的代码,把assimp中模型的顶点信息都存储到VBO里面了,并且帮我们绑定到VAO中了,我们只需要在着色器按照顺序获取数据即可。

一个顶点有如下的信息:

// render data unsigned int VBO, EBO;// initializes all the buffer objects/arraysvoid setupMesh(){// create buffers/arraysglGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);glGenBuffers(1, &EBO);glBindVertexArray(VAO);// load data into vertex buffersglBindBuffer(GL_ARRAY_BUFFER, VBO);// A great thing about structs is that their memory layout is sequential for all its items.// The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which// again translates to 3/2 floats which translates to a byte array.glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);// set the vertex attribute pointers// vertex PositionsglEnableVertexAttribArray(0);  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);// vertex normalsglEnableVertexAttribArray(1);    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));// vertex texture coordsglEnableVertexAttribArray(2);  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));// vertex tangentglEnableVertexAttribArray(3);glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));// vertex bitangentglEnableVertexAttribArray(4);glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));// idsglEnableVertexAttribArray(5);glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));// weightsglEnableVertexAttribArray(6);glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights));glBindVertexArray(0);}

这里也帮我们绑定了贴图文件,对应的命名规则要按照这里来:比如texture_diffuse+num

    void Draw(Shader &shader) {// bind appropriate texturesunsigned int diffuseNr  = 1;unsigned int specularNr = 1;unsigned int normalNr   = 1;unsigned int heightNr   = 1;for(unsigned int i = 0; i < textures.size(); i++){glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding// retrieve texture number (the N in diffuse_textureN)string number;string name = textures[i].type;if(name == "texture_diffuse")number = std::to_string(diffuseNr++);else if(name == "texture_specular")number = std::to_string(specularNr++); // transfer unsigned int to stringelse if(name == "texture_normal")number = std::to_string(normalNr++); // transfer unsigned int to stringelse if(name == "texture_height")number = std::to_string(heightNr++); // transfer unsigned int to string// now set the sampler to the correct texture unitglUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);// and finally bind the textureglBindTexture(GL_TEXTURE_2D, textures[i].id);// TODO : Pass tangential space data to shaders}// draw meshglBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);glBindVertexArray(0);// always good practice to set everything back to defaults once configured.glActiveTexture(GL_TEXTURE0);}

3.2 顶点着色器

按照布局顺序获取顶点信息,然后求出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 aBitTangent;out vec3 tFragPos;
out vec2 texCoords;
out vec3 tViewPos;
out vec3 tLightPos;uniform vec3 viewPos;
uniform vec3 lightPos;uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;void main()
{gl_Position = projection * view * model * vec4(aPos,1.0f);texCoords = aTexCoords;// calculate TBNmat3 normalMatrix = transpose(inverse(mat3(model)));vec3 T = normalize(normalMatrix * aTangent);vec3 N = normalize(normalMatrix * aNormal);T = normalize(T - dot(T,N) * N);        //Gram-Schmidt processvec3 B = normalize(cross(T,N));mat3 TBN = transpose(mat3(T,B,N));            //the transpose and inverse is the sametFragPos = TBN * vec3(model * vec4(aPos,1.0f)); // this should be in tangent spacetViewPos = TBN * viewPos;tLightPos = TBN * lightPos;
}

注意mat3(T,B,N)是按照列排,需要转置。然后BitTangent是没必要的,否则不能保证正交化。

3.3 片元着色器

只要所有向量在切线空间计算即可,注意命名规则,这里用的是Blinn-phong光照模型+光线衰减。

#version 330 corein vec2 texCoords;
in vec3 tFragPos;
in vec3 tViewPos;
in vec3 tLightPos;uniform vec3 lightColor;
uniform float lightLinear;
uniform float lightQuadratic;uniform sampler2D texture_diffuse1;
uniform sampler2D texture_specular1;
uniform sampler2D texture_normal1;
//uniform sampler2D texture_height1;out vec4 fragColor;
void main()
{vec3 Normal = texture(texture_normal1, texCoords).rgb;Normal = normalize(Normal * 2 - 1.0f) * 1.06f;vec3 Diffuse = texture(texture_diffuse1, texCoords).rgb;float specStrength = texture(texture_specular1,texCoords).r;// then calculate lighting as usualvec3 ambient = vec3(0.3 * Diffuse);vec3 lighting  = ambient; vec3 viewDir  = normalize(tViewPos - tFragPos); // diffusevec3 lightDir = normalize(tLightPos - tFragPos);vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * lightColor;// specularvec3 halfwayDir = normalize(lightDir + viewDir);  float spec = pow(max(dot(Normal, halfwayDir), 0.0), 32.0);vec3 specular = lightColor * spec * 0.1f;// attenuationfloat distance = length(tLightPos - tFragPos);float attenuation = 1.0 / (1.0 + lightLinear * distance + lightQuadratic * distance * distance);diffuse *= attenuation;specular *= attenuation;lighting += diffuse + specular;fragColor = vec4(lighting , 1.0f);
}

注意的是,法线需要转换到【-1,1】,否则不应该高光的地方会出现,高光部分会非常明显。

最后得到的效果应该如下所示:

法线贴图的实现【OpenGL】相关推荐

  1. LearnOpenGL笔记——五、高级光照:“法线贴图”和”视差贴图“

    五.高级光照:"法线贴图"和"视差贴图" 5.4 法线贴图 以光照算法的视角考虑的话,只有一件事决定物体的形状,这就是垂直于它的法线向量 砖块表面只有一个法线向 ...

  2. OpenGL生成的法线贴图并增加光照

    这一篇将由OpenGL生成法线贴图的基础上再增加光照效果. 思路如下: 准备一张墙壁图片A. 通过A自动生成法线贴图. 设计一个平行光,指定平行光的光照颜色和光照方向. 使用漫反射光照公式,法线贴图和 ...

  3. OpenGL shader normals法线贴图的实例

    OpenGL shader normals法线贴图 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <glad/glad.h> #in ...

  4. openGL法线贴图和纹理贴图结合使用,以增强三维物体表面细节

    openGL系列文章目录 文章目录 openGL系列文章目录 前言 一.法线贴图? 二.代码 1.主程序 2.着色器程序 运行效果 源码下载 前言 凹凸贴图的一种替代方法是使用查找表来替换法向量.这样 ...

  5. OpenGL 法线贴图Normal Mapping

    OpenGL法线贴图Normal Mapping 法线贴图Normal Mapping简介 法线贴图 切线空间 手工计算切线和副切线 切线空间法线贴图 复杂物体 最后一件事 法线贴图Normal Ma ...

  6. opengl高级光照之法线贴图

    法线贴图 opengl官方文档 核心修改的就是片段着色器中的normal值 uniform sampler2D normalMap; void main() { // 从法线贴图范围[0,1]获取法线 ...

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

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

  8. OpenGL通过原图自动生成法线贴图

    这种生成法线贴图的效果并不是很好,最新的思路是使用基于cGANs的方法来生成法线贴图. glsl比较简单的算法,思想有点类似于人工智能中的梯度下降,步骤为: 将像素看作向量,计算出模长,代表为像素的高 ...

  9. Learn OpenGL 笔记6.5 Normal Mapping(法线贴图)

    我们通过在这些平面三角形上包裹 2D 纹理来增强真实感,隐藏多边形只是很小的平面三角形的事实. 从照明技术的角度来看,确定对象形状的唯一方法是通过其垂直法向量. 这种使用每片段法线与每表面法线相比的技 ...

  10. openGL之API学习(五十七)法线贴图、色彩贴图、高光贴图

    Normal map : Normal map (法线贴图) 它的作用是模拟出高模上的一些细节纹理,特别是将高模上的圆滑和粗糙度投射到低模上,让低模也有高模的效果. 因为高模的面数非常多,导入引擎后电 ...

最新文章

  1. ajax用https请求不了_Chrome滚动事件概率性Block Ajax请求
  2. WildFly 报错 java.lang.NoClassDefFoundError
  3. java基础之堆、栈、方法区 继承 多态
  4. Python 技术篇-使用pygame库展示界面添加图片不显示问题解决办法
  5. Java 时间日期整理
  6. restful web_泽西岛的RESTful Web服务
  7. ARM汇编中ldr伪指令和ldr指令(转载)
  8. 查看eclipse中项目部署位置
  9. Nacos整合SpringCloud的自动注册原理
  10. linux下RRDTool安装方法
  11. UNIX 操作系统体系结构调整
  12. 博途v14电脑要求_博图TIA V14版本完整体验加测试
  13. 在Ubuntu 16.04 64bit上安装谷歌地球Google Earth免费版
  14. 电子秤查看通道及更改通道方法
  15. 学做衣服论坛 -服装DIY教程,缤纷服装网,裁剪教程,家用缝纫机,买布料
  16. Android数据编码之Base64
  17. 音视频6.2——相机采集数据编码成H264
  18. 键盘按键与键码的对照表的对照表
  19. mysql自动生成id方式_Mysql全局ID生成方法
  20. 强烈推荐:一款中文AI问答、创作、绘画工具

热门文章

  1. 2021.11.19【读书笔记】丨snakemake常见问题汇总(下)
  2. Arduino上U8g2库自定义中文库的经历
  3. 金融科技方便生活,分布式架构助力微粒贷“闪电放款”
  4. 大咖说开源|郑振宇:通过开源手段巩固基础软件供应链
  5. Git系列之设置邮箱和用户名
  6. iOS自学-混合编程
  7. 如何重设思科路由器密码并保持配置不丢失?
  8. python蓝牙连接测试_基于python实现蓝牙通信代码实例
  9. DSP TMS320C5509A 控制DDS AD9854芯片进行AM-MSK调制
  10. 一个非常简单的爬取网站图片的Python爬虫实例