写给笨人的法线贴图原理 【转】
我算个笨人吧.笨人以前弄懂一些东西后,讲给笨人听往往更有效.看之前请自行具备图形学关于光照的基础知识.
>> world/object space normal map
我们先讲基于世界或模型坐标的法线贴图(world/object space normal map).不常用,但是基础.
首先,请无视你之前google到的所谓通过Photoshop生成法线贴图类似文章,美术除外.那只是一种利用近似hack的手法利用法线贴图原理.无助于理解真正的过程.不过看完这文章后,你应能理解Photoshop的这种做法的来历.
不搞清法线贴图的生成原理,是无法正确理解之后shader中的计算使用的.法线贴图的出现,是为了低面数的模型模拟出高面数的模型的"光照信息".光照信息最重要的当然是光入射方向与入射点的法线夹角.法线贴图本质上就是记录了这个夹角的相关信息.光照的计算与某个面上的法线方向息息相关.
我们知道计算机里的模型,是通过多个多边形面组合来近似模拟一个物体的.它不是圆滑的.面数越多,则越接近真实物体.光照到某个面当中的一点时,法线是 通过这个面的几个顶点通过插值得到的.插值其实也是为了模拟这个点"正确"的法线方向,不然整个面所有点的法线一致的话,光照上去,我们看到的模型夸张点 就像一面面镜子拼接起来了.但法线插值不可避免的仍然会失真.模型的面数越高,失真的程度自然越小.要是能无限细分到人眼看不出的地步,根本不用插值了.
面数高,需要计算的量和内存需求就高.前辈找到了法线贴图(前身是凹凸贴图)这个办法,使低模能够近似享受高模的光照细节信息.代价是有的,就是需要一 个记录这些信息的文件.这是程序中常用的存储空间换计算时间的做法.3D程序中偏爱使用这个手法.谁叫存储硬件的单位价格比计算硬件的单位价格降低速度快 很多呢.
显卡包括包括与之相辅的图形api,读的数据最初来源是图片.所以记录这个信息的文件就被我们保存为图片格式.法线贴图后边2个字就这么来的.很好你已经明白一半了.
我们再来弄清另一半.
因为面数少,低模上某个区域的一个面,可能就是高模上相同区域的几个面.看下图的高模与低模的对比(为了简便我们抽象为2维的线段)
上边凹凹凸凸的曲线表示高模.下边比较平滑的表示低模.因为高模细节多,所以在某段区域它的方向变化自然比平平板板的低模多.上图看不懂我表示无能为力.
看到这图,一些人应该有所感觉了.没感觉也不要紧,接下再来.
不管高模还是低模,反正最后还是要被上色的.假设模型已经被渲染完成有颜色了,现在我们想象用剪刀把模型展开(类似给动物扒皮的过程),得到2张差不多 一样大小的皮,毕竟面积不会差太多.高模的皮当然肤白体嫩精度高,低模的皮就有些糙了.现在再想象这么个过程:逐渐把高模的皮移到低模的皮上方一定高度直 到水平重叠.
现在这个样子你有感觉没有?没感觉也不要紧,接下再来.
虽然模型精度不一样,无论如何,这2张皮每一点都是有颜色了的(插值的功劳).两张皮上相同一点的颜色,高模这张皮上的更真实,因为在计算最终颜色信息 所依赖的法线,高模上的点比低模上的点更精确.我们如何给低模这张皮美容,使它能够接近高模的效果呢?换句话说,找到办法,使土肥圆演变为黑木耳,质变为 白富美是不可能的,那得下辈子.
办法很暴力.现在再想象你用一根针,从上往下,刺穿高模的皮,再刺到低模的皮.保证针垂直,这样就刺到同一点了.再想象如果这针有魔力的话,它刺穿高模 皮的过程中,盗取了一些信息,传送到低模皮上边.低模皮依靠这些信息计算,成功蜕变为黑木耳.这些信息是什么呢?当然是法线信息了.现在高模这张皮被密密 麻麻插满了针眼,换句话说,保存高模泄漏来的信息,必定是点对点的.即这张皮上的每个点,都得被保存.所以法线贴图跟原始的贴图是一样大小的,贴图内每个 点都保存了对应高模某个点的法线信息.实际的计算,只会关心由贴图里得来的法线信息,低模上的那些法线,被抛弃了.
现在这个样子你有感觉没有?没感觉也不要紧,接下再来.
如何赋予这根针魔力呢?宅男们,甘道夫是帮不了你的忙的.伟哥也帮不了你.只有数学,才能拯救世界...
为什么我之前强调垂直呢?不只是为针能扎到同一点.现在请把这个过程,想象到上图中.图中的箭头,表示高模上某个点的法线方向.如何记录这个方向信息? 现在请想象逐渐把高模和低模重叠在一起,为了方便想象,低模小一些被高模包住了.或者你干脆想象高模的面在低模面的正上方,或一个圆球里有一个内切的正多 面体.再想象有一束光线(针的等价物),从上往下照射,把高模上的法线投射到低模上.
现在你有感觉了吧.
前戏大功告成,现在我们来处理稍微细节些的问题了.这是一个投影过程.但是影子是2维的啊?向量是由x,y,z三个分量构成的.高模上某点投影到低模上 对应点所在平面,只剩2个分量的投影了.好比我们现在只知道法线在x-y平面的投影方向,那在z轴的方向呢?只要我们确保投影前法线是单位向量,那很简单 z=1-x*x-y*y.这样我们还可以省下保存z的空间.其实我们既然已经知道这个法线方向(高模object space内的法线方向),而且被单位化了,直接保存也是可以的.投影过程只是个思想实验,实际是不会有什么光线由上到下投射的.
到此可以明确了,"正统"的法线贴图生成,是高模,低模不可缺一的.因为没有高模就不知道法线方向,没有低模,就不知道高模上某点的法线对应于低模上哪个点.
因为某点的法线信息是被保存到法线贴图上对应像素点的.实际计算是把法线x,y,z方向大小映射到颜色空间rgb里.就是把x值存在r里,把y值存在g 里,把z值存在b里.因为rgb是8字节为单位的.所以高模的法线信息存储到像素里是要丢失精度的.而且前面计算高模与低模对应点也不可能完全匹配到,本 来就是个模拟过程.自然法线贴图也不是无敌的.
现在我们可以回答之前的Photoshop根据diffuse贴图生成法线贴图的问题了.实际的diffuse贴图是根本没有包含模型上的法线信息的. 因此它根据diffuse贴图得出的法线贴图根本就是错误的.但为什么能够应用呢?请想象高模的精度高的吓人,高到渲染后把高模皮扒下来后,就成了一张照 片.再想象之前高模上的贴图是布满了铁锈.于是你就得到了一张铁锈照片.Photoshop处理这张铁锈照片,其实是根据一些算法(sobel等等)把颜 色值转化为梯度值,近似模拟了法线.因为我们其实不关心铁锈的精确分布,像那么一回事就可以了,所以这种情况下如此处理是可以将就的,坑坑洼洼效果最适合 如此做法.photoshop这种脱离高模低模的做法容易让人迷惑,导致新手以为法线是从diffuse贴图上来的,或者干脆被阻断了思路.
我们上边计算法线贴图所用到的法线,又是从哪里来的.如果这个法线方向,是处于世界坐标中的(world space),那称为world space normal.如果是处于物体本身局部坐标中的,那称为object space normal.很容易想象,world space normal一旦从贴图里解压出来后,就可以直接用了,效率很高.但是有个缺点,这个world space normal 是固定了,如果物体没有保持原来的方向和位置,那原来生成的normal map就作废了.因此又有人保存了object space normal.它从贴图里解压,还需要乘以model-view矩阵转换到世界坐标,或者转换到其他坐标取决于计算过程及需求.object space normal生成的贴图,物体可以被旋转和位移.基本让人满意.但仍有一个缺点.就是一张贴图只能对应特定的一个模型,模型不能有变形(deform).
>> tangent space normal map
为解决适应变形的normal map,我们仍能从这两种方法中得到启示.world space normal直接保存的是世界坐标系中的高模法线方向.因此低模取出该点法线就可以直接使用,前提是低模的世界坐标系与高模一致,一点旋转都不能有,不然 法线方向就改变了.object space normal保存的是模型空间坐标系中的高模方向,低模取出该点取出来法线,还需要乘以所在的model-view矩阵,转化为低模的世界坐标系中的方 向,也就是说低模端还需要做一个运算.因此即使低模任意旋转也不怕,有model-view矩阵可以把法线贴图中的值转换两者效率由高到低,灵活度由低到 高.问题来了,我们是否能找到高模上的另外一个坐标系统,使低模变形也时也能较正确的变换法线到世界坐标系中?
我们考察一下object space.当一个低模旋转时,因为是刚体不变形,相当于每个点都乘以一个旋转矩阵R,之后各点关系保持不变.实际上,我们保持物体不旋转,将 object space的坐标系(x,y,z三个轴)旋转,得到的结果是一样的.这个关系相信大家都能理解.换句话说,法线针对于object space是固定不动的,物体保持在object space固定,只管跟随坐标系的移动,旋转就行了.现在我们想象低模的某个点需要变形时,那原则上也可以通过让object space坐标系乘以某个变形矩阵T来达到.但是不同的点有不同的变形,不可能存在一个矩阵T即适合这个点又适合这个点.因此object space坐标系是不能用的.会有哪个单一的坐标系能存在一个所有点都共用的变形矩阵吗?显然无法想象.
变形时,顶点关系改变了,即面的形状,方向改变了.如果面上存在一个固定的坐标系,那当物体变形,移动,旋转时,这个坐标系必定跟着面一起运动,那么在 这个坐标系里的某个点或向量(比如我们把高模法线转换到这个坐标系里),不需要变动.当整个面发生变化时,我们只需要计算面上的坐标系到世界坐标系的转换 矩阵,那么定义在这个面上的点或坐标(固定的),乘以这个矩阵即可得到在世界中的坐标.这个坐标如何构造目前对我们不重要,请务必理解这个概念.我们不过 是寻求一个局部坐标系,局部坐标系中的点坐标,乘以局部坐标系到世界坐标系的转换矩阵(这个矩阵是低模渲染时动态计算的的),得到局部坐标系中的点在世界 坐标系中的坐标.这样法线贴图中存储的固定的值(法线方向),才能进行有意义的计算.
看到这里很明显的,这种做法需要数千个不同的定义在面上的坐标系.低模上有多少个面,就得有多少个这样的坐标系.这种方法的计算量自然是比object space normal map要大一些的.在低模的每个面上,要构造出这个坐标系.这个坐标系术语里称为tangent space.
object space normal map的中,低模的object space坐标系与高模中的object space坐标系是重合的.所以不需要构建,所以低模上某点才能直接用高模的法线替换自己的法线.坐标系重合这个概念很重要.新方法中,低模上的这个 tangent space,也必须与高模上的坐标系tangent space.因为低模上的一个面,可能对应了高模上的几个面(精度高),按照新方法每个面都有一个局部坐标系,那对于低模上的每个面,高模因为存在好几个 面,就会出现好几个局部坐标系,这肯定是不行的.所以高模所用的tangent space,就是低模上的.生成法线贴图,必定会确认高模上哪些面都对应低模上的哪个面,然后高模上的这几个面的法线,都会转换为低模这个面上所构建的 tangent space的坐标.这样,当低模变形时,即三角面变化时,它的tangent space也会跟着变化,保存在贴图里的法线乘以低模这个面的tangent space到外部坐标系的转换矩阵即可得到外部坐标.顺便再提一点,高模保存的这个法线,是高模上object space里的法线,看到这里你该明白这是自然而然的.你搜索文章时可能会看到什么把光转换到tangent space里,确保处于同一个坐标系下的话.确实是这样.但初次接触却还是模糊.我以为确保tangent sapce重合及做法,才是让人顿悟tangent space的诀窍点.
上图稍微清楚一些了.曲线表示高模,P点处有TBN坐标系.线段表示低模,M点处有T'B'M坐标系.高模上P点处的法线转换到TBN坐标里,与N的夹 角为NPN'.低模处取出这个法线为N'',与低模的PM(面法线)夹角为PMN''.可以看到这2个夹角是近似的.所以渲染时高模上的法线,是基于低模 上的这个坐标系运算的.你可能会说,我这的TBN不就是实际的高模面上的左边吗?别忘了很可能几个高模的面挤在一起对应一个低模的面,高模的这个TBN必 然是经过一种“插值”或“平均”得来的,实际上也会跟低模有些相关性,以求最匹配的效果。具体如何得来未深究,有高手可否告知否?
当我自己想到上边这段话时,tangent space的法线贴图原理就豁然开朗了.接下来我们构建这个tangent space坐标系.
面在动时,tangent space也得跟着动.面上的垂直法线是跟着动的,因此这个法线N可以作为tangent space的一个坐标轴.非常非常需要注意的是,这里所说的面上垂直法线,不是指插值所得来的法线,那个法线正是是我们需要保存的内容.N单纯就是指垂直于这个面的方向.
我们考察上图,对于一个三角面,它的边V2V1,V3V1,V3V2我们总是能够确定的.边也定会在变形时跟着动.因此我们可以选择一条边作为 tangent space的第二个坐标轴T.第三个坐标轴就简单了,直接根据叉积来B=T * N.这个坐标轴就订好了.其实,坐标轴的选定几乎可以是任意的,只要你能够确保每次都能构建出来.比如你可以先选择V1V3,V1V2作为坐标 轴,N=V1V3 * V1V2.这里N恰好和前面一样方向.但如此一来这个坐标系中V1V2,V1V3不是垂直的,不正交的坐标基在矩阵运算中是不方便的,还得正交化.因此我 们选择第一种最直观最清晰最方便的方法.
既然三个坐标轴都确定了,那构建object space到tangent space的矩阵O-TBN就简单了,我们把T,B,N单位化,分别作为tangent space的x,y,z轴.根据三个坐标基我们构造矩阵如下:
O-TBN =
高模上object space内的某点法线(不会是world space的,否则旋转就露相),乘以这个矩阵,即得到tangent space内的法线方向,再把这个值映射到rgb空间,存为贴图即可.这个矩阵为什么是这样,这是题外话了.我简略说一下:object space的三个坐标轴(1,0,0),(0,1,0),(0,0,1)乘以这个矩阵,必须刚好为tangent space中的坐标轴,很自然矩阵就是上边的样子.而object space其他点的坐标都是x,y,z三个单位坐标的线性组合.所以这个矩阵对于其他点必定是正确的.
实际上在vertex shader中,我们只能知道当前顶点的信息,三角形的另外两个顶点我们是不知道的.但现代的shader能够为顶点提供一个tangent信息,表示在 顶点处的切线.你可以想象一个足球上经过某点的切线.因此我们会把顶点的tangent方向作为上边的T向量.这也是tangent space叫这个名称的由来.你会看到很多文章中提到纹理的u,v方向.因为面上某点的u,v是沿各条边线性插值的,所以u,v方向与边的方向相同.其实 我们现在已经有现成的tangent可以用了.
现在我们可以分析为什么tangent space法线贴图是偏蓝色了.因为对于高模上的面来说,因为精度太高(面很小,而且周围的面相对它的方向很平滑),所以这个面渲染时计算机认为这个面 的"弯曲"程度很小,即面上各个点插值得来的法线相互间偏差很小.基本跟整个面的垂直方向不会差太多.因此在tangent space里,这些法线都跟z轴偏差较小.而z轴是被保存在贴图里的b字节处(蓝色通道)里.所以贴图显示出来的颜色就偏蓝了.
好了,现在高模面上各点的法线值,都转换为低模上的tangent space坐标了.现在我们考虑具体的低模上的渲染计算了.假设在低模上的某个面我们计算出了这个矩阵,并取出了面上某点的对应在法线贴图里法线值.现在 需要计算光照.我们可以把光向量转换到tangent space里做计算.也可以把得到的法向量转换到world space与光向量进行计算.结果是一样的.实际考量,你会发现后一种方法不好.因为对于面上的每个点,都要计算一次normal到world space的准换.而前一种方法,对一个面上的所有点,只要计算一次光向量到tangent space的计算.然后再考虑到vertex shader与fragment shader的流程,你会发现刚好我们可以在vertex shader计算光线到tangent space的转换,在fragment sader取出法线值与前面得到的tangent space里的光线方向做计算即可.这里提醒一下,一般verteix shader中我们得到的光线方向是基于world space的,而法线贴图保存的是高模的object space内的方向然后再转换到tangent space,所以在vertex shader中,我们必须先把光线先转换到object space,再转换到tangent space.这样才能保证最终计算时,光线与法线是基于同一个坐标系的.这也是你在很多normal map的shader里,看到类似ToOjectSpaceDir(lightDir)之类函数的原因,正是要把光转换到object space.
实际做法可能会有些复杂.比如有些模型是镜像对称,贴图也是镜像对称的,计算时会省去另一半等等.这时如何处理看具体的法线贴图生成软件和处理它的引擎(shader)了.基本原理还是上边所说的.
tangent space normal map适应变形的这种能力,使它不仅能够应用在原来的模型上边,甚至可以应用在变形严重的不同模型上.即法线贴图有一定的脱离原来模型使用的能力.比如你 模拟出了一个高精度的粗糙花岗岩平板表面,得出的法线贴图可以应用到圆柱模型上边.类似photoshop的直接根据花岗岩表面照片生成法线贴图也是能够 使用的.因为目地就是为了微小的扰动法线生成凹凸不平的表面.虽然这个表面并不是正确的还原一个真正存在的花岗岩表面.但图形学不就是一个模拟过程,能足 够真实的欺骗我们的眼睛就行.其实这种粗糙表面微小扰动只是应用之一.你搜到一些处理的好的图片,不仔细看你会发现低模的怪物会以假乱真体现出高模才有的 平滑弯曲.
以上是法线贴图的原理.因为该原理的应用范围很广,也能够串起很多知识点,是非常值得搞清楚的.我当初在学习法线贴图过程中,没有看到能够讲的让我清晰明白整个过程的.遂作此篇.