转自: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. centos使用更新更快的yum源

    The Remi Repo is a yum repository maintained by a French dude - Remi Collet. It contains much more u ...

  2. 研究AVCaptureDevice

    一.Apple Resource 1. wwdc 2014: Camera Caputre: Manual Controls 2. Exaple code: AVCam&AVCamManul ...

  3. AVRStudio 的编译优化级别

    -00 无优化. -01 减少代码尺寸和执行时间,不进行需要大量编译时间的优化. -02 几乎执行所有优化,而不考虑代码尺寸和执行时间. -03 执行 -02 所有的优化,以及内联函数,重命名寄存器的 ...

  4. 几个Google中国的访问IP

    前面几个IP的访问速度比较快. 74.125.31.106 173.194.45.20 173.194.45.19 173.194.45.18 173.194.45.17 173.194.45.16 ...

  5. vi编辑器的常见使用技巧

    光标移动 在普通模式下, 1.按 h 向左移动光标 按 h + 数字n 可以向右移动 n个字符   比如 h + 5  就是向左移动5个字符 2.按j向下移动光标 3.按k向上移动光标 4.按 l 向 ...

  6. 刘汝佳 算法竞赛-入门经典 第二部分 算法篇 第六章 1(Lists)

    127 - "Accordian" Patience 题目大意:一个人一张张发牌,如果这张牌与这张牌前面的一张或者前面的第三张(后面称之为一位置和三位置)的点数或花式相同,则将这张 ...

  7. Difference Between Initialization and Assignment in C++

    Initialization happens when a variable is given a value at the moment it is created. Assignment obli ...

  8. Javascript offsetLeft详情

    <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <m ...

  9. 传说中的WCF(9):流与文件传输

    在使用Socket/TCP来传输文件,弄起来不仅会有些复杂,而且较经典的“粘包”问题有时候会让人火冒七丈.如果你不喜欢用Socket来传文件,不妨试试WCF,WCF的流模式传输还是相当强大和相当实用的 ...

  10. 【LA3942-Remember the word 】Trie

    http://acm.hust.edu.cn/vjudge/problem/22109 题意:给定n个单词,一个字符串,问把这个字符串划分为若干个单词的连接(单词可重复使用)有多少种方案(mod200 ...