3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇

摘要:
当然,仅仅是这种简单的压缩是不够的,但是传统的纹理压缩方法如DXTC等,不能直接对法线贴图使用,需要进行一定的变化,具体方法将在下一篇博客《3D游戏常用技巧NormalMapping原理解析——高级篇》进行详细描述。

1、法线贴图基本概念

在制作3D游戏时,常常遇到这样一个问题:一个平面,这个平面在现实中并不是一 个“平”面,例如砖墙的表面带有石质浮雕等等。这种情况下如果只是简单的做一个平面,则让人感觉严重失真,如图1所示;而如果用很密集的三角形去表示这类略有凹凸的表面,则性能上大大下降。研究人员发现,人眼对物体的凹凸感觉,很大程度上取决于表面的光照明暗变化,如果能通过一张贴图在一个平面上表现出由凹凸造成的明暗变化,则可以让人眼感觉这个平面是凹凸不平的(虽然这个平面还是平的)。法线贴图正是为了这个目的而产生的。

3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇第1张

图1 不同细节程度的蜡烛

准确的说,法线贴图是Bump Mapping(凹凸贴图)的其中一种。第一个Bump Mapping由Blinn在1978年提出,目的是以低代价给予计算机几何体以更丰富的表面信息。30年来,这项技术不断延展,尤其是计算机图形学成熟以后,相继出现了不少算法变体,法线贴图就是其中很重要的一种。研究人员对法线贴图进一步改进,出现了Parallax Mapping(视差贴图), Relief Mapping等技术,实现了更逼真的效果。本文仅针对法线贴图进行介绍。

一条法线是一个三维向量,一个三维向量由x, y, z等3个分量组成,在法线贴图中,把(x, y, z)当作RGB3个颜色的值存储(如图2),并将其每个分量映射到[-1, 1]。例如,对于x, y, z各有8位的纹理,[0, 128, 255]表示法向量(-1, 0, 1)。

3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇第2张

图2 利用彩色通道存储法线贴图

2、切线空间

法线贴图中存储的法线最初是定义在世界空间中,但在实际中,这种方式很少见,因为只要物体移动,法线贴图则不再有效。另一种方式就是将法线存储在物体的局部空间中,物体可以进行刚体变换(平移,旋转,缩放),法线贴图依旧有效,但是这种方法并不能应对任何方式的变换,并且法线贴图不能在不同物体进行复用,增加了美工的负担。所以,现在现在普遍采用的解决方案是将法线存储在切线空间中。

简单的来说,切线空间可以理解为纹理空间的u,v方向和法向量n,这3个方向构成了切线空间,一般使用T、B和N表示,如图3所示。具体解释见http://blog.csdn.net/bonchoix/article/details/8619624,这个博客解释的很好,没见过的童鞋可以看一看,第一次理解可能会有点困难。

3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇第3张

图3 切线空间

3、法线贴图的使用

法线贴图的制作过程如图4所示,通过对比高模和低模的差异,生成法线贴图,在运行时使用法线贴图+低模即可表现出接近高模的效果,但是大大降低了显卡的负担。计算法线贴图的算法在3ds Max中已实现,美术人员只需针对同一个模型制作一个高模和一个低模,烘焙出一张法线贴图,在游戏中使用低模+法线贴图即可。

3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇第4张

图4 法线贴图制作过程

在渲染模型表面的时候,需保证光照方向和法线是处在同一坐标空间。光照方向在世界空间中,而法线在切线空间,将他们变换到同一坐标空间无非两种方法:

1、将光照方向变换到切线空间;

2、将法线变换到世界空间。

在大部分情况下,使用第一种方案会比较好,因为只需针对每个顶点进行变换,然后在三角形中对变换后的光照方向进行插值;而使用第二种方案,则需要对每个像素进行变换。显然,在光源较少的情况下,使用第一种方案可以减少计算量。当然,在光源数量较多的情况下,到底使用那种方案,需要值得考虑,因为使用第二种方案只需要变换一次法线,而第一种方案需要有多个光照方向需要变换。

值得注意的是,第一种方案中的插值对于点光源来说存在一个近似,光照方向在一个三角形上变化的很慢,所以可以进行插值,不过对于方向光则没有这个问题。

4、法线贴图效果

法线贴图+低模可以表现出接近高模的效果,在图4中可以看到,加上法线贴图的低模,效果很接近高模;图5为unity3d中法线贴图的示例,左图(没有法线贴图)和右图(有加入法线贴图)相比,明显右图的立体感更强。

3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇第5张  3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇第6张

图5 unity3d中法线贴图的示例。左图中的方块没有法线贴图,右图加入了法线贴图

5、法线贴图的压缩和mipmap

归一化的法线长度为1,且在切线空间下,法线的z分量不可能为负数,所以只需要存储x和y值即可。当然,仅仅是这种简单的压缩是不够的,但是传统的纹理压缩方法如DXTC等,不能直接对法线贴图使用,需要进行一定的变化,具体方法将在下一篇博客《3D游戏常用技巧Normal Mapping (法线贴图)原理解析——高级篇》进行详细描述。

使用传统mipmap方法生成的法线贴图对于漫反射表面基本没问题,但是在镜面表面会导致严重的视觉问题。对于漫反射表面来说,光照的计算公式为l·nl为光线方向的相反方向,n为法线,l·n1+l·n2+l·n3+l·n4 = l·(n1+n2+n3+n4) / 4,而mipmap则是事先计算(n1+n2+n3+n4) / 4,所以对于漫反射表面,对法线贴图使用传统方式的mipmap基本没问题。为什么是基本没问题而不是完全没问题呢?因为这里存在一个近似,若l·n< 0,则光照值为0(光照不能为负),若将这个因素考虑进去,漫反射表面也会有问题,不过在实际当中这种情况表现不明显,所以可以认为基本没问题。

对于镜面表面来说,当视线偏离反射光线方向的时候,光照强度会急剧下降,反映在公式中是因为其含有cosm(h·n)项(具体公式可以Google),而漫反射光照是线性变化,所以对于镜面表面,不能使用传统方法生成法线贴图的mipmap,需要用特殊的技术手段进行处理,具体方法见下一篇博客“3D游戏常用技巧Normal Mapping (法线贴图)原理解析——高级篇”。

参考资料

[1]http://blog.sina.com.cn/s/blog_5497d613010006qu.html

[2]http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map.html

[3]http://blog.csdn.net/bonchoix/article/details/8619624

[4]Akenine-Möller T, Haines E, Hoffman N. Real-time rendering 3 [M].

免责声明:文章转载自《3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇PL/SQL Developer导入导出Oracle数据库方法JBox插件总结下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

随便看看

破解SQLYog30天试用方法

删除此项,SQLyog重新启动后将重新生成注册表。使用寿命为30天。...

VS中,如何将存在于解决方案里,但是没有显示出来的文件(或文件夹)显示到项目中。

不知道有没有人跟我一样,刚开始接触VS的时候,没有通过“右键-˃添加”产生文件,而是直接一些文件或者文件夹建在了项目的本地目录中。导致最后这些文件无法在项目中显示。其实方法很简单如图“Test”文件夹下有一个“Test2”没有显示出来。点击工具栏“显示所有文件”这时就发现之前没有显示的文件就都显示出来了。在想要显示的文件上点击右键,然后点击“包括在项目中”完...

使用jsPlumb插件实现动态连线功能

jsPlumb是一个强大的JavaScript连线库,它可以将html中的元素用箭头、曲线、直线等连接起来,适用于开发Web上的图表、建模工具等,其实jsPlumb可能主要是用来做流程图的,它在实现这方面的功能上非常强大,我在项目中只使用了它少部分功能,来实现项目中连线的效果。...

koroFileHeader插件快速入门使用教程

插件下载插件可以直接在vscode的扩展中查找koroFileHeader,但是有时候由于网络的问题会查找不到软件。插件配置koroFileHeader支持许多功能,但是不是所有功能都是需要,我们关注往往是如何配置注释内容和注释的一些选项。"fileheader.cursorMode":{//这部分是函数头的配置},"fileheader.customMad...

【转载】 银河麒麟V10系统安装U盘制作

在制作U盘安装盘的过程中,Kylin系统的ISO映像文件比较大,因此很耗时。创建完成后,“写入硬盘映像”对话框将自动关闭。...

Qt自定义Widget中设置滚动条

用代码自定义一个独立显示的Widget,要在里面画图,同时加上滚动条,滚动条:  printerYuLanPainter=newQPainter();test=newQScrollArea();printerYuLanWidget=newQWidget(test);printerYuLanWidget-˃setMaximumSize(720,1000);pr...