« SwapBuffers的等待,虚伪的FPSshader复习与深入:Normal Map(法线贴图)Ⅱ »

shader复习与深入:Normal Map(法线贴图)Ⅰ

Normal Map法线贴图,想必每个学习计算机图形学的人都不陌生。今天在这里按我的理解总结一下,作为复习,也作为深入学习吧。——ZwqXin.com

自从看完那本《数学在计算机图形学上的应用》后,一直想好好地真正实践一次法线贴图/凹凸贴图呢(以前是根据橙书弄了一下罢了)。昨天偶尔看到篇涉及BumpMap的文,正好觉得是个机会,便在网上狂找相关资料——果然,越看越觉得自己还有很多理论的地方需要弄明白呢。

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

说起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,NzT,正是该向量在模型空间的表示。其他两轴同理。说明该矩阵把切线空间的坐标系统转换到模型空间了(一切变换都是在变换坐标系[乱弹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
Nx
T′y
B′y
Ny
T′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实现代码一起,我会在下篇谈及,请留意了哦。
shader复习与深入:Normal Map(法线贴图)Ⅱ

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

  • quote 1.wuwenxi
  • 我看着都懂,可就是没说怎么求一个像素的切空间。都是理论
    zwqxin 于 2009-12-13 13:09:59 回复
    建议去看看文中间给出的文章链接Tangent Space,像素的切向量就是normalmap自身.P.S.非理论部分请看<下篇>.



  • 2009-12-12 21:17:26 回复该留言
  • quote 2.okaylin
  • 总结得很好,图看不见,麻烦楼主补一下吧。
    okaylin 于 2012-1-3 15:00:29 回复
    用了《显示本站所有图片》方法,依然看不到图
    zwqxin 于 2012-1-4 22:13:52 回复
    应该是picasa服务器在中国的问题。唉。
  • 2012-1-3 14:52:05 回复该留言
  • quote 3.fmisser
  • 博主你好,我请教个问题,如你文章所述两种策略计算光照,第二种策略在vertex 到 pixel 光栅化时差值光源向量从而会有偏差,但对于结果影响不大。
    对于第一种策略如果在vertex shader 中 获取该vertex的 T,B, N向量(我是研究D3D的,通过DirectX API 可获得),然后转换到世界坐标系,传给pixel shader,然后构造TBN矩阵并在pixel shader 中 将normal map 的pixel normal 乘以该TBN矩阵获得世界坐标系法线向量,再计算具体光照,这样做由于本身模型是低精度的而Normal Map是模型高精度下构造的,所以 光栅化的时候TBN向量很明显也有偏差,但是我看到很多资料确实这样做的,不知道博主是否有尝试过此种策略,效果如果? 由于我本身也是个新手,所以其中 有什么理解不正确的地方还请博主纠正。
    zwqxin 于 2012-6-5 21:35:11 回复
    光栅化的插值的结果一般都会带来你所谓的“偏差”的吧。即使是文中我用的第二种方法,其实光源向量等都会要经过插值(当时我文中也提出疑问)。所以如果只是诸如游戏之类应用的话,我个人觉得真没必要太在乎这些“偏差”的,效果可以接受即可。
  • 2012-6-4 16:18:06 回复该留言
  • quote 4.zfree
  • 博主你好,新手请教个问题,
    1.为什么normal map的法向量是相对像素切空间,而不相对模型坐标系,再将光线向量转换到模型坐标系,这样不能在同一坐标系。
    2.像素的切空间中的N向量和法线贴图中对应像素存储的法线向量有什么差别?
    感觉有点理解模糊,希望博主能赐教·
    zwqxin 于 2012-6-6 21:00:26 回复
    1.首先纹理不该与具体模型数据直接挂接,其次也难制作这样的normalmap;2.如果没理解错你的问题,那就是TBN矩阵变换前后的区别……
    zfree 于 2012-6-6 21:32:28 回复
    第一个问题后来我想想的确不应该在模型空间,灵活性降低,比如在做蒙皮动画时,模型空间到每个像素切空间就不一定是一致的。
    第二个问题,我是不是可以理解成这样,这里像素的切空间N向量应该是指像素所在的三角形的法向量, 所以文中以三角形顶点的法向量来近似?
    zwqxin 于 2012-6-8 0:48:35 回复
    你说的切空间N向量是指什么?原生的三角形顶点法向量跟normalmap上的法线没啥直接关系的,这里它的作用只是联合切线副法线构造TBN矩阵把法线图本地坐标的normal转换到模型上(形象点就是经过转换后,normalmap横竖两轴与模型上具体某顶点/像素的法线副法线重合时,转换后的map上的法线值就是我们需要的。至于转换后连带着会让normalmap上虚拟的垂直于纹理平面那条轴会跟三角形顶点/像素法向量重合,而已。)
    maval 于 2013-8-4 19:48:22 回复
    法线贴图存储值有几种类型的,例如dot3就是切线空间法线,最常见,还有存储模型空间的以及世界空间的。
  • 2012-6-6 18:10:34 回复该留言
  • quote 5.芸朵糖
  • 博主你好,小菜鸟想请教一个问题,我想用阴影图的方法做平行光源的阴影,因此做了亮点修改:1,将传入着色器的投影矩阵改为正视矩阵,2,将向量L=lightPos-V.xyz改为L=lightPos,即表示方向;但得出的结果是阴影图可以画出来,但到了场景里就没有了阴影,而是黑条,请问我这个方法是哪里出问题了呢~~~请求博主回复啊~~叩谢~~~^^
    zwqxin 于 2012-7-25 21:18:11 回复
    印象中我也没试过directional……单看你的描述没看出很明显的问题,可以先尝试调整一下depth bias或投影矩阵看看。
  • 2012-7-25 18:08:14 回复该留言

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

IE下本页面显示有问题?

→点击地址栏右侧【兼容视图】←

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

Copyright 2008-2013 ZwqXin. All Rights Reserved. Theme edited from ipati.