法线贴图的实现【OpenGL】
文在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】相关推荐
- LearnOpenGL笔记——五、高级光照:“法线贴图”和”视差贴图“
五.高级光照:"法线贴图"和"视差贴图" 5.4 法线贴图 以光照算法的视角考虑的话,只有一件事决定物体的形状,这就是垂直于它的法线向量 砖块表面只有一个法线向 ...
- OpenGL生成的法线贴图并增加光照
这一篇将由OpenGL生成法线贴图的基础上再增加光照效果. 思路如下: 准备一张墙壁图片A. 通过A自动生成法线贴图. 设计一个平行光,指定平行光的光照颜色和光照方向. 使用漫反射光照公式,法线贴图和 ...
- OpenGL shader normals法线贴图的实例
OpenGL shader normals法线贴图 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <glad/glad.h> #in ...
- openGL法线贴图和纹理贴图结合使用,以增强三维物体表面细节
openGL系列文章目录 文章目录 openGL系列文章目录 前言 一.法线贴图? 二.代码 1.主程序 2.着色器程序 运行效果 源码下载 前言 凹凸贴图的一种替代方法是使用查找表来替换法向量.这样 ...
- OpenGL 法线贴图Normal Mapping
OpenGL法线贴图Normal Mapping 法线贴图Normal Mapping简介 法线贴图 切线空间 手工计算切线和副切线 切线空间法线贴图 复杂物体 最后一件事 法线贴图Normal Ma ...
- opengl高级光照之法线贴图
法线贴图 opengl官方文档 核心修改的就是片段着色器中的normal值 uniform sampler2D normalMap; void main() { // 从法线贴图范围[0,1]获取法线 ...
- OpenGL.Shader:9-学习光照-法线贴图(计算TBN矩阵)
OpenGL.Shader:9-学习光照-法线贴图(计算TBN矩阵) 这次文章学习法线贴图,法线贴图在游戏开发和GIS系统开发当中尤为广泛,其表现力特别的强,绘制的效果特别接近真实.更重要的一点就是, ...
- OpenGL通过原图自动生成法线贴图
这种生成法线贴图的效果并不是很好,最新的思路是使用基于cGANs的方法来生成法线贴图. glsl比较简单的算法,思想有点类似于人工智能中的梯度下降,步骤为: 将像素看作向量,计算出模长,代表为像素的高 ...
- Learn OpenGL 笔记6.5 Normal Mapping(法线贴图)
我们通过在这些平面三角形上包裹 2D 纹理来增强真实感,隐藏多边形只是很小的平面三角形的事实. 从照明技术的角度来看,确定对象形状的唯一方法是通过其垂直法向量. 这种使用每片段法线与每表面法线相比的技 ...
- openGL之API学习(五十七)法线贴图、色彩贴图、高光贴图
Normal map : Normal map (法线贴图) 它的作用是模拟出高模上的一些细节纹理,特别是将高模上的圆滑和粗糙度投射到低模上,让低模也有高模的效果. 因为高模的面数非常多,导入引擎后电 ...
最新文章
- ajax用https请求不了_Chrome滚动事件概率性Block Ajax请求
- WildFly 报错 java.lang.NoClassDefFoundError
- java基础之堆、栈、方法区 继承 多态
- Python 技术篇-使用pygame库展示界面添加图片不显示问题解决办法
- Java 时间日期整理
- restful web_泽西岛的RESTful Web服务
- ARM汇编中ldr伪指令和ldr指令(转载)
- 查看eclipse中项目部署位置
- Nacos整合SpringCloud的自动注册原理
- linux下RRDTool安装方法
- UNIX 操作系统体系结构调整
- 博途v14电脑要求_博图TIA V14版本完整体验加测试
- 在Ubuntu 16.04 64bit上安装谷歌地球Google Earth免费版
- 电子秤查看通道及更改通道方法
- 学做衣服论坛 -服装DIY教程,缤纷服装网,裁剪教程,家用缝纫机,买布料
- Android数据编码之Base64
- 音视频6.2——相机采集数据编码成H264
- 键盘按键与键码的对照表的对照表
- mysql自动生成id方式_Mysql全局ID生成方法
- 强烈推荐:一款中文AI问答、创作、绘画工具
热门文章
- 2021.11.19【读书笔记】丨snakemake常见问题汇总(下)
- Arduino上U8g2库自定义中文库的经历
- 金融科技方便生活,分布式架构助力微粒贷“闪电放款”
- 大咖说开源|郑振宇:通过开源手段巩固基础软件供应链
- Git系列之设置邮箱和用户名
- iOS自学-混合编程
- 如何重设思科路由器密码并保持配置不丢失?
- python蓝牙连接测试_基于python实现蓝牙通信代码实例
- DSP TMS320C5509A 控制DDS AD9854芯片进行AM-MSK调制
- 一个非常简单的爬取网站图片的Python爬虫实例