笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

游戏画面中的美术品质对产品来说非常重要,这决定了产品是否能吸引玩家。美术品质的好坏主要体现在材质的渲染上,材质的渲染不仅是美术的事情也是程序的事情,二者要互相配合才能得到想要的效果。本篇博客主要介绍的是材质的法线渲染,本篇博文也适合美术人员学习,当然对于程序更重要,它从法线的原理讲起,逐步深入,博文最后会给出源代码。

游戏场景中会摆满很多物体,其中每个物体都可能由成百上千平坦的三角形组成。我们以向三角形上附加纹理的方式来增加额外细节,提升真实感,隐藏多边形几何体是由无数三角形组成的事实。纹理确有助益,然而当你近看它们时,这个事实便隐藏不住了。现实中的物体表面并非是平坦的,而是表现出无数(凹凸不平的)细节。

例如,砖块的表面。砖块的表面非常粗糙,显然不是完全平坦的:它包含着接缝处水泥凹痕,以及非常多的细小的空洞。如果我们在一个有光的场景中看这样一个砖块的表面,问题就出来了。下图中我们可以看到砖块纹理应用到了平坦的表面,并被一个点光源照亮。

光照并没有呈现出任何裂痕和孔洞,完全忽略了砖块之间凹进去的线条;表面看起来完全就是平的。我们可以使用specular贴图根据深度或其他细节阻止部分表面被照的更亮,以此部分地解决问题,但这并不是一个好方案。我们需要的是某种可以告知光照系统给所有有关物体表面类似深度这样的细节的方式。

如果我们以光的视角来看这个问题:是什么使表面被视为完全平坦的表面来照亮?答案会是表面的法线向量。以光照算法的视角考虑的话,只有一件事决定物体的形状,这就是垂直于它的法线向量。砖块表面只有一个法线向量,表面完全根据这个法线向量被以一致的方式照亮。如果每个fragment都是用自己的不同的法线会怎样?这样我们就可以根据表面细微的细节对法线向量进行改变;这样就会获得一种表面看起来要复杂得多的幻觉:

每个fragment使用了自己的法线,我们就可以让光照相信一个表面由很多微小的(垂直于法线向量的)平面所组成,物体表面的细节将会得到极大提升。这种每个fragment使用各自的法线,替代一个面上所有fragment使用同一个法线的技术叫做法线贴图(normal mapping)或凹凸贴图(bump mapping)。应用到砖墙上,效果像这样:

你可以看到细节获得了极大提升,开销却不大。因为我们只需要改变每个fragment的法线向量,并不需要改变所有光照公式。现在我们是为每个fragment传递一个法线,不再使用插值表面法线。这样光照使表面拥有了自己的细节。

为使法线贴图工作,我们需要为每个fragment提供一个法线。像diffuse贴图和specular贴图一样,我们可以使用一个2D纹理来储存法线数据。2D纹理不仅可以储存颜色和光照数据,还可以储存法线向量。这样我们可以从2D纹理中采样得到特定纹理的法线向量。

由于法线向量是个几何工具,而纹理通常只用于储存颜色信息,用纹理储存法线向量不是非常直接。如果你想一想,就会知道纹理中的颜色向量用r、g、b元素代表一个3D向量。类似的我们也可以将法线向量的x、y、z元素储存到纹理中,代替颜色的r、g、b元素。法线向量的范围在-1到1之间,所以我们先要将其映射到0到1的范围:

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

将法线向量变换为像这样的RGB颜色元素,我们就能把根据表面的形状的fragment的法线保存在2D纹理中。在博客文章开头展示的那个砖块的例子的法线贴图如下所示:

这会是一种偏蓝色调的纹理(你在网上找到的几乎所有法线贴图都是这样的)。这是因为所有法线的指向都偏向z轴(0, 0, 1)这是一种偏蓝的颜色。法线向量从z轴方向也向其他方向轻微偏移,颜色也就发生了轻微变化,这样看起来便有了一种深度。例如,你可以看到在每个砖块的顶部,颜色倾向于偏绿,这是因为砖块的顶部的法线偏向于指向正y轴方向(0, 1, 0),这样它就是绿色的了。

在一个简单的朝向正z轴的平面上,我们可以用这个diffuse纹理和这个法线贴图来渲染前面部分的图片。要注意的是这个链接里的法线贴图和上面展示的那个不一样。原因是OpenGL读取的纹理的y(或V)坐标和纹理通常被创建的方式相反。链接里的法线贴图的y(或绿色)元素是相反的(你可以看到绿色现在在下边);如果你没考虑这个,光照就不正确了(使用SOIL载入纹理会上下颠倒,它也会把法线在y方向上颠倒)。加载纹理,把它们绑定到合适的纹理单元,然后使用下面的改变了的像素着色器来渲染一个平面:

uniform sampler2D normalMap;  void main()
{           // 从法线贴图范围[0,1]获取法线normal = texture(normalMap, fs_in.TexCoords).rgb;// 将法线向量转换为范围[-1,1]normal = normalize(normal * 2.0 - 1.0);   [...]// 像往常那样处理光照
}

这里我们将被采样的法线颜色从0到1重新映射回-1到1,便能将RGB颜色重新处理成法线,然后使用采样出的法线向量应用于光照的计算。在例子中我们使用的是Blinn-Phong着色器。

通过慢慢随着时间慢慢移动光源,你就能明白法线贴图是什么意思了。运行这个例子你就能得到本篇博客开始的那个效果:

实现上述效果的源代码,顶点着色器代码如下所示:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;// Declare an interface block; see 'Advanced GLSL' for what these are.
out VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;
} vs_out;uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;void main()
{gl_Position = projection * view * model * vec4(position, 1.0f);vs_out.FragPos = vec3(model * vec4(position, 1.0));vs_out.TexCoords = texCoords;mat3 normalMatrix = transpose(inverse(mat3(model)));vs_out.Normal = normalMatrix * normal;
}

片段着色器代码如下所示:

#version 330 core
out vec4 FragColor;in VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;
} fs_in;uniform sampler2D diffuseMap;
uniform sampler2D normalMap;  uniform vec3 lightPos;
uniform vec3 viewPos;
uniform bool normalMapping;void main()
{           vec3 normal = normalize(fs_in.Normal);if(normalMapping){// 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);   }// Get diffuse colorvec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;// Ambientvec3 ambient = 0.1 * color;// Diffusevec3 lightDir = normalize(lightPos - fs_in.FragPos);float diff = max(dot(lightDir, normal), 0.0);vec3 diffuse = diff * color;// Specularvec3 viewDir = normalize(viewPos - fs_in.FragPos);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.0f);
}

然而有个问题限制了刚才讲的那种法线贴图的使用。我们使用的那个法线贴图里面的所有法线向量都是指向正z方向的。上面的例子能用,是因为那个平面的表面法线也是指向正z方向的。可是,如果我们在表面法线指向正y方向的平面上使用同一个法线贴图会发生什么?

光照看起来完全不对!发生这种情况是平面的表面法线现在指向了y,而采样得到的法线仍然指向的是z。结果就是光照仍然认为表面法线和之前朝向正z方向时一样;这样光照就不对了。下面的图片展示了这个表面上采样的法线的近似情况:

你可以看到所有法线都指向z方向,它们本该朝着表面法线指向y方向的。一个可行方案是为每个表面制作一个单独的法线贴图。如果是一个立方体的话我们就需要6个法线贴图,但是如果模型上有无数的朝向不同方向的表面,这就不可行了。

注意事项:实际上对于复杂模型可以把朝向各个方向的法线储存在同一张贴图上,你可能看到过不只是蓝色的法线贴图,不过用那样的法线贴图有个问题是你必须记住模型的起始朝向,如果模型运动了还要记录模型的变换,这是非常不方便的;如果把一个diffuse纹理应用在同一个物体的不同表面上,就像立方体那样的,就需要做6个法线贴图,这也不可取。

另一个稍微有点难的解决方案是,在一个不同的坐标空间中进行光照,这个坐标空间里,法线贴图向量总是指向这个坐标空间的正z方向;所有的光照向量都相对与这个正z方向进行变换。这样我们就能始终使用同样的法线贴图,不管朝向问题。这个坐标空间叫做切线空间(tangent space)。

下篇博客介绍切线空间。。。。。。。。。

OpenGL核心技术之法线贴图相关推荐

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

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

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

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

  3. OpenGL 核心技术之立方体贴图

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D ...

  4. OpenGL核心技术之切线空间

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者;已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解 ...

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

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

  6. OpenGL 法线贴图Normal Mapping

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

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

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

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

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

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

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

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

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

最新文章

  1. 【 MATLAB 】ellip 函数介绍(椭圆滤波器设计)
  2. 使用MAP文件快速定位程序崩溃代码行
  3. Visual Studio 2017 - Update 2预览版已发布
  4. linux服务 运维案例,linux运维实战练习案例-2015年12月20日-12月31日
  5. 所谓高情商就是会说话--总结
  6. oracle 11g初始段大小,Oracle 11g配置调优,一般需要注意哪些方面呢?
  7. 【JMX】JMX 远程 连接 The client has been closed
  8. pcl中ransac提取直线_多目标跟踪中的相机运动模型
  9. 腾讯广告“虚拟IP”赛题突出重围,入选第七届“互联网+”双创大赛产业命题
  10. 使用Java的MessageDigest实现MD5加密算法
  11. 小程序加入人脸识别_微信小程序实现人脸识别
  12. 在线Api接口,网易云音乐api数据完整接口文档,QQ音乐在线api接口文档,电商api开放数据接口文档分享,小说ap接口,漫画api接口
  13. 服务器存档修改器,太吾绘卷存档修改器v2.6
  14. c语言如何表示大于小于等于,Excel 公式中大于和小于等于计算应怎么写
  15. java皮丘 博客园,又一个设计工具 Framer X Preview
  16. 什么是Java的反射机制
  17. 微生物组+代谢组联合分析
  18. List多条件组合排序
  19. iOS使用TestFlight进行App构建版本测试
  20. MySQL数据库三大范式和反范式

热门文章

  1. 广告学概论--名词解释
  2. MongoDB安装Python操作MongoDB
  3. 数据分析指标缩写英文单词解释
  4. 装配图中齿轮的画法_装配图的视图和画法
  5. cracking the pm interview_2020泰晤士报THE世界大学排名发布!如何凭艺术冲进大U名校?...
  6. 哈特公社,在家也能轻创业
  7. 浅谈GOF设计模式之建造者模式(五)
  8. 育儿书籍阅读顺序的建议
  9. 淘宝/天猫/京东/拼多多/苏宁易购/小米商城/华为商城/抖音快手直播/茅台抢购助手,宝惠抢购助手/OK助手源码
  10. 如何从官网下载Chrome浏览器离线安装包