转自:http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map.html
Normal Map法线贴图,想必每个学习计算机图形学的人都不陌生。今天在这里按我的理解总结一下,作为复习,也作为深入学习吧。——ZwqXin.com
自从看完那本《数学在计算机图形学上的应用》后,一直想好好地真正实践一次法线贴图/凹凸贴图呢(以前是根据橙书弄了一下罢了)。昨天偶尔看到篇涉及BumpMap的文,正好觉得是个机会,便在网上狂找相关资料——果然,越看越觉得自己还有很多理论的地方需要弄明白呢。
说起Normal Map(法线贴图),就会想起Bump Map(凹凸贴图)。Bump Mapping是Blin大师在1978年提出的图形学算法,目的是以低代价给予计算机几何体以更丰富的表面信息(高模盖低模)。30年来,这项技术不断延展,尤其是计算机图形学成熟以后,相继出现了不少算法变体,90年代末的Normal Map解放了必须自行计算纹理像素法线的痛苦,新世纪以来相继又出现了Parallax Mapping, Relief Mapping等技术。抛开那些无聊的概念区分,它们的本体还是Bump Map,目的也是一致的。
1. 传统的Bump Map
如果你对纯净的Bump Map有兴趣,A Practical and Robust Bump-mapping Technique for Today's GPU应该是值得一看的论文。说Today,其实是GDC 2000的事情了,但对于传统的Bump Map的理论是很丰富的,我是没精力看完它啦……
那时候的Bump Map须要我们计算纹理图上每个像素的法线信息,简单的还可能做到,对复杂的纹理要搞清面光背光份量简直要命,于是就用Height Map,在一张高度图上记录每个像素对应的纹理位置的高度信息(这个比较容易办到,NEHE22也是这类)。看上去就是一张地形网格——这样的话,计算每个像素点的法线就不那么难了。XY方向相邻像素的高度相减就是两条正交的切向量,叉乘外加左/右手定则就获得法线。或者更精确点,用八邻域弄个边缘检测算子(sobel、拉普拉斯之类 )[图像处理里的空间域滤波],或者应用斜坡法([水效果Ⅲ - 抖动波] )来求切线、法线。

2. 制作NormalMap
但是这样还是挺麻烦的,既然都动用额外的贴图了,何不把这些与实现无关的预处理——作为结果的法线信息——都放进纹理里呢?这就是Normal Map的思想起源。但是,谁来做这样的一张法线图呢?敲定美工了。每个像素的RGB分别存储该像素对应法线的XYZ分量,只要把法线的分量由(-1,1)映射成(0,255)就可了。观察一张法线图,以蓝色为主,是因为朝向图面外的法线(0,0,1)都被编码成(0,0,127)了(读入OpenGL后即(0,0,0.5)),而图上越红的地方表明法线越向右,越绿的地方表明法线越向上,就可以理解了。总体来说,就是一张紫蓝色的图。怎么做这样的图呢?当然最好是有一个工具,输入原图和高度图后执行上述的算法得出新图了,事实上已经有很多这类工具了(譬如比较著名的photoshop的NV插件Normal Map Filter,甚至不用高度channel也可[效果- -]),以下几篇文章有详细介绍,有兴趣的可以看一看:
Tutorial On Normal Mapping (PHOTOSHOP [ENGLISH])
怎样用PhotoShop创建Bump Map图像 (PHOTOSHOP [CHINESE])
Nvidia Normal Map 插件参数之详解 (PHOTOSHOP [翻译])
GIMP normalmap plugin   (GIMP   [ENG])
关于NormalMap制作的原理,更详细的可参考此文:Normalmap原理及去除接缝
 3. 切线空间(Tangent Space)
其实这个概念前文已经提及了。每个像素根据高度图生成的三轴坐标系,就是被称为切线空间坐标系的东西,每个像素人手一个。可见Normal Map里面每个像素的法线就是定义在这个切线空间的。注意,这些法线是属于像素的,而不是顶点,我们平时用的法线是顶点法线,是定义在模型坐标系的[乱弹OpenGL中的矩阵变换(上)] ,定义于所属物件的唯一的局部坐标系原点之上。而这些像素法线定义于切线坐标系,其原点就在该像素上,切线副法线在法线的垂直平面上。

(表面依然是平的,但通过搅动法线,使进入我们眼睛的光线强度不一,模拟出凹凸面漫反射的特点。图from GDNet)
应用这些像素法线的目的无非是计算出该像素的OutPut颜色:col = baseColor * (amb + diffuse) + specular。这些都应该在像素着色器(fragment shader)里进行,因为我们要做的是针对每个像素的处理[Shader快速复习:Per Pixel Lighting(逐像素光照)] 。其中需要用到像素法线的是diffuse和specular(以前是用通过顶点法线线性插值而来的normal),法线分别与光线向量、半向量作点乘得到对应因子。这个因子是个夹角cos而已,所以只要满足像素法线与两个向量单位化并在同一坐标系下(而无论是哪个坐标系),夹角就是一定的。这样看来,两个选择:
1. 把像素法线都从各自的切线空间转到视图空间来,再点乘;
2.把光线向量、半向量从视图空间转到像素各自的切空间来,再点乘。
很多文章一口咬定就是第2种好,原因是第1种要变换N个量;第2种只变换2个量。仔细分析,其实两种选择变换的次数是一样的,都是2*N。说第2种好,是因为:
第1种必须在fragment shader里进行,对象是从Normal Map读出的像素法线和经过线性插值而来的两个向量,它们不是同一坐标系的,按描述应该是各像素法线乘以各自一个的变换矩阵,转到视图空间来,但确实没有其他的可提供构筑这个矩阵的信息了,若有可能应该就是另外的varying变量传入了;
第2种可以选择在vertex shader里进行,但是能不能就在这里变换到切线空间呢?假设可以,那么得到的针对顶点的数值在光栅化-线性插值后能否满足呢?
要回答这个问题,还得考虑像素的切线空间和顶点的切线空间之间的关系。是的,顶点法线也可以变换到切线空间,但这有什么用呢?一步一步来吧。先考虑切线空间在OpenGL世界里的次元位置:

(from paulsprojects)
为什么是紧挨模型坐标系呢?其实想想也能理解,在上面谈及切线坐标系的时候,并没有广阔的“世界”这个概念。只针对每个像素/顶点,无疑是比模型坐标系更狭隘的“世界观”,所以那个位置是适合的(箭头方向无所谓,坐标系之间是可以相互转换的)。其实对于某个具体的物体上的像素/顶点,你可以考虑那是把模型空间的原点平移到该像素/顶点上,各模型坐标系方向轴向量一起经过旋转,使Z轴与像素/顶点的法线重合,XY轴分别与像素/顶点的切线副法线重合——这只是一个仿射变换而已,如同模型/世界/视图空间之间的变换一样。
如果你记得图形学书上关于世界/视图空间的变换矩阵的构建的话,就更容易理解这样的形式了。从切线空间到模型空间的变换矩阵(TBN矩阵MTBN)为:

其中T,B,N是定义在模型空间的该像素/顶点的“切/副法/法向量”。稍微检验一下,考虑某个三角面上的某个顶点,其法线充当切线空间的Z轴,在切线空间中表示为(0,0,1),在OpenGL里解释为一个列向量(0,0,1)T,用上面的矩阵MTBN左乘该向量,得到(Nx,Ny,Nz)T,正是该向量在模型空间的表示。其他两轴同理。说明该矩阵把切线空间的坐标系统转换到模型空间了(一切变换都是在变换坐标系[乱弹OpenGL中的矩阵变换(上)] )。当然这是特例说明,但确实这个矩阵包含仿射矩阵里的旋转元素了(它只包含旋转,不设置平移,是因为我们只需要它来变换向量,向量是可以任意平移的,若要弄完整的4X4矩阵,第4列平移列就是该顶点模型坐标)。具体推导也不难,随便Google一下"tangent space"就出来一堆了,而且都是基本一样的推导过程,推一个:Tangent Space。
其逆变换(矩阵MTBN-1)就可以把向量从模型空间变换到对应顶点的切线空间了。如果你确保T,B,N两两垂直,这个正交矩阵的逆矩阵就是其转置矩阵,这很理想。但万一你不确保这点(涉及到具体应用,很多问题的,后面会说),就保证它们大致满足三叉状,用所谓的Gram-Schmidt 算法矫正:
T′ = T − (N · T)N
B′ = B − (N · B)N − (T′ · B)T′
反正最后得到的是这样的形式——用它左乘光源向量和半向量,就得到对应于该顶点切线空间的光源向量和半向量了:
T′x
B′x
NxT′y
B′y
NyT′z
B′z
Nz

为什么是顶点?因为这是你唯一能取得其切线/副法线/法线的东西了。这也是之前说的选择1不行的原因,在那张Normal Map里面已经没有任何法线副法线的确实信息了(只知道它们在法线垂直平面上),即使能通过别的方法取得(起码要增加传入数据),那要在fragment shader里每像素人手又计算一个矩阵,这就又是一个“计算量”(不是次数)的问题。所以还是用选择2吧,也就是上面矩阵MTBN-1的讨论。
选择2的第一个问题现在很清楚了:是可以的。只要取得顶点的切线/副法线/法线数据就能建立矩阵并变换光源向量和半向量,但结果是针对顶点的,我们需要的是针对像素的。光栅化线性插值这两个向量,就是对应像素的值,但这对吗?直觉上不对,但结果显示这样做没有不妥(或者说不会与真实所须差太多)。一般文章都没有直接透视这个问题,其实考虑一个矩形平面就露馅了,它四个顶点的TBN一致,变换得的光源向量也该一致,插值后得光源向量也该一致,但NormalMap中的像素有各自不同的切线空间系统,光源向量不该一致的呃(虽则同向光源、不同法线足够形成凹凸效果)。所以我对选择2的第二个问题保持疑问,有道深者请为鄙人指点迷津!
反正即使计算两向量夹角的计算可能会有偏差,也不会太离谱,问题到此结束。至于有的文章提及对diffuse的计算,光源向量插值后不须再归一化的问题(我尝试过,整体会变暗一点),就不深入了。注意我们在vertex shader里变换到切线空间的是模型空间下的光源向量和视线向量(半向量是它们的和),而一般这两个向量定义在视图空间,所以之前还要做一个视图空间->模型空间的变换(用ModelView矩阵的逆矩阵)。这是很多文章囫囵掉的一点。但如果你能取得视图空间下的顶点TBN,也不需。因为切线/副法线/法线若是被变换到视图空间,则上面的TBN矩阵MTBN就是把东西从该顶点的切线空间变换到视图空间(道理是一样的),MTBN-1就能把视图空间下的这两个向量变换到该顶点的切线空间(参见下篇的代码)。
 最后的问题:怎么去取得模型空间下的顶点的切线,副法线,法线?连同shader实现代码一起,我会在下篇谈及,请留意了哦。

本文来源于ZwqXin http://www.zwqxin.com/ , 转载请注明
原文地址:http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map.html

shader复杂与深入:Normal Map(法线贴图)1的更多相关文章

  1. 3D游戏常用技巧Normal Mapping (法线贴图)原理解析——高级篇

    1.概述 上一篇博客,3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇,讲了法线贴图的基本概念和使用方法.而法线贴图和一般的纹理贴图一样,都需要进行压缩,也需要生成mipm ...

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

    http://www.cnblogs.com/wangchengfeng/p/3470310.html

  3. Normal Map中的值, Tangent Space, 求算 Tangent 与 Binormal 与 TBN Matrix

      - Normal Map中的值 -   有没有想过,Normal Map(法线贴图)为什么看上去都是“偏蓝色”的?这是因为,在map中存储的值都是在Tangent Space(切空间)下的.比如, ...

  4. [转]Normal Map中的值, Tangent Space, 求算 Tangent 与 Binormal 与 TBN Matrix

    原文出处 https://www.cnblogs.com/lookof/p/3509970.html - Normal Map中的值 -   有没有想过,Normal Map(法线贴图)为什么看上去都 ...

  5. 【转载】法线贴图Nomal mapping 原理

    法线贴图多用在CG动画的渲染以及游戏画面的制作上,将具有高细节的模型通过映射烘焙出法线贴图,贴在低端模型的法线贴图通道上,使之拥有法线贴图的渲染效果,却可以大大降低渲染时需要的面数和计算内容,从而达到 ...

  6. (转)Unity3D 游戏贴图(法线贴图,漫反射贴图,高光贴图)

    原帖网址http://www.u3dpro.com/read.php?tid=207  感谢jdk900网友的辛苦编写 我们都知道,一个三维场景的画面的好坏,百分之四十取决于模型,百分之六十取决于贴图 ...

  7. 【Unity Shader】六、使用法线贴图(Normal Map)的Shader

    学习资料: http://www.sikiedu.com/course/37/task/456/show# http://www.sikiedu.com/course/37/task/458/show ...

  8. 翻译:非常详细易懂的法线贴图(Normal Mapping)

    翻译:非常详细易懂的法线贴图(Normal Mapping) 本文翻译自: Shaders » Lesson 6: Normal Mapping 作者: Matt DesLauriers 译者: Fr ...

  9. [Unity Shader] 切线空间的法线贴图

    切线空间的法线贴图,可以这样理解: #纹理坐标是从0到1,它的坐标是x向右,y向下 #顶点坐标是从-1到-1,坐标是x向右,y向上 1 由表面上某点的切线Tangent.副切线Bitangent.法线 ...

随机推荐

  1. 四大机器学习降维算法:PCA、LDA、LLE、Laplacian Eigenmaps

    四大机器学习降维算法:PCA.LDA.LLE.Laplacian Eigenmaps 机器学习领域中所谓的降维就是指采用某种映射方法,将原高维空间中的数据点映射到低维度的空间中.降维的本质是学习一个映 ...

  2. PE文件结构学习

    PE:Portable Executable File Format(可移植的执行体).Windows平台主流可执行文件格式..exe与.dll文件都是PE格式.32位的叫做PE32,64位的叫做PE ...

  3. JavaScript之Throw、Try 、Catch讲解

    try 语句测试代码块的错误. catch 语句处理错误. throw 语句创建自定义错误. 错误一定会发生 当 JavaScript 引擎执行 JavaScript 代码时,会发生各种错误: 可能是 ...

  4. MYSQL导入导出.sql文件(转)

    一.MYSQL的命令行模式的设置: 桌面->我的电脑->属性->环境变量->新建-> PATH=“:path\mysql\bin;”其中path为MYSQL的安装路径. ...

  5. HTML5中表单验证的8种方法(转)

    在深人探讨表单验证之前,让我们先思考一下表单验证的真实含义.就其核心而言,表单验证是一套系统,它为终端用户检测无效的控件数据并标记这些错误.换言之,表单验证就是在表单提交服务器前对其进行一系列的检查并 ...

  6. sublime 配置C++

    1. 安装C语言编译器MinGW,并把MinGW安装目录下的bin目录添加到环境变量PATH里.详细方法参照MinGW安装和使用 2. 在SublimeText安装目录下的Data\Packages\ ...

  7. Android 4.4 KitKat, the browser and the Chrome WebView

    Having V8 as the JavaScript engine for the new web view, the JavaScript performance if much better, ...

  8. cache应用(asp.net 2.0 SQL数据缓存依赖 [SqlCacheDependency ] )

    Asp.net 2.0 提供了一个新的数据缓存功能,就是利用sql server2005 的异步通知功能来实现缓存 1.首先在sqlserver2005 中创建一个test的数据库. 在SQL Ser ...

  9. 火狐和IE之间的7个JavaScript差异

    尽管 JavaScript 历史上使用冗长而令人生厌的代码块来标的特定浏览器的时期已经结束了,但是偶尔使用一些简单的代码块和对象检测来确保一些代码在用户机器上正常工作依然是必要的. 这篇文章中,我会略 ...

  10. javascript实现数据结构:串--定长顺序存储表示以及kmp算法实现

    串(string)(或字符串)是由零个或多个字符组成的有限序列.串中字符的数目称为串的长度.零个字符的串称为空串(null string),它的长度为零. 串中任意个连续的字符组成的子序列称为该串的子 ...