说起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就能把视图空间下的这两个向量变换到该顶点的切线空间(参见下篇的代码)。

最后的问题:怎么去取得模型空间下的顶点的切线,副法线,法线?

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

1.
怎样获得顶点的TBN

其实我觉得这个是实践部分最麻烦的地方。OpenGL提供了诸如glNormal、normal-vbo之类的接口设置顶点的法线,然后在shader中
以gl_Normal等方式取得顶点法线数据,但是没有提供切线和副法线的。当然两者只要其一就足够了(另一者可通过叉乘和左/右手定则获得)。因为要把
TBN导入shader,干脆就设置attribute变量,记录每个顶点的切线。切线一般就是相邻顶点的差向量了(其实这有时候是非常繁重的工作)。

如果是通常的3DS模型的话,顶点法线是共顶点的面的面法线的加权,这样法线就不一定垂直于某个面,即与切线不垂直。但只要它们还是近似垂直的,上篇提及的Gram-Schmidt
算法应该可以处理。或者在shader中,把法线与切线叉乘出副法线,再用法线与副法线叉乘得新的切线,也能确保两两垂直。这样之前的TBN矩阵的转置矩阵就能直接作为其逆矩阵,完成向量从模型坐标系往切线空间坐标系的变换了。

问题不只这样。对于一些模型,共享顶点的三角面片面法线差角太大,这时候计算出的该顶点法线和切线就可能带来麻烦。在橙书(OpenGL
Shading
Language)中,谈及了切线必须是一致的(consistently),面片相邻的顶点切线不应该差距太大。但若相邻面片夹角太大,得到的该顶点法
线就可能与“共享该顶点的面片”上的其他顶点的法线差异很大,从而切线也会相差很大,直接导致光向量等在这两顶点的切线空间差异很大,插值的各个针对像素
的光向量方向差异很大,与像素法线点乘的cos也会差异得很明显(而现实中一般的凹凸面漫反射光线不会有太大方向差异)。解决方法是把该出了问题的顶点拆
成两个(原地拷贝,3DS模型就不用了-
-),一个面片用一个,其法线只受所属的面片的面法线决定(这样最后会形成突出的边缘,但夹角大的面片之间实际上就应该会是有这样的效果吧)。

另一个问题,我们向shader传入顶点法线切线,希望副法线由两者叉乘得出。但既然叉乘就有个方向问题(结果可以有两个方向,AXB与BXA是不一样的,我以前弄shadow
volume就曾被它这种特性作弄过)。AXB改成BXA实际上会导致凹凸感反向,原来凹的变凸了,原来凸的变凹了(要仔细比对,不然会有首因效应)。一般就用N
X T吧,因为基本上都是这个顺序的,结果也符合原Normal Map。

2.
GLSL 1.2 Shader实现代码

没什么好说的,就是前面算法翻译成GLSL。

Vertex
Shader:

  1. // vertex shader
  2. uniform vec3 lightpos; //传入光源的模型坐标吧
  3. uniform vec4 eyepos;
  4. varying vec3 lightdir;
  5. varying vec3 halfvec;
  6. varying vec3 norm;
  7. varying vec3 eyedir;
  8. attribute vec3 rm_Tangent;
  9. void main(void)
  10. {
  11. vec4 pos = gl_ModelViewMatrix * gl_Vertex;
  12. pos = pos / pos.w;
  13. //把光源和眼睛从模型空间转换到视图空间
  14. vec4 vlightPos = (gl_ModelViewMatrix * vec4(lightpos, 1.0));
  15. vec4 veyePos   = (gl_ModelViewMatrix * eyepos);
  16. lightdir = normalize(vlightPos.xyz - pos.xyz);
  17. vec3 eyedir = normalize(veyePos.xyz - pos.xyz);
  18. //模型空间下的TBN
  19. norm = normalize(gl_NormalMatrix * gl_Normal);
  20. vec3 vtangent  = normalize(gl_NormalMatrix * rm_Tangent);
  21. vec3 vbinormal = cross(norm,vtangent);
  22. //将光源向量和视线向量转换到TBN切线空间
  23. lightdir.x = dot(vtangent,  lightdir);
  24. lightdir.y = dot(vbinormal, lightdir);
  25. lightdir.z = dot(norm     , lightdir);
  26. lightdir = normalize(lightdir);
  27. eyedir.x = dot(vtangent,  eyedir);
  28. eyedir.y = dot(vbinormal, eyedir);
  29. eyedir.z = dot(norm     , eyedir);
  30. eyedir = normalize(eyedir);
  31. halfvec = normalize(lightdir + eyedir);
  32. gl_FrontColor = gl_Color;
  33. gl_TexCoord[0] = gl_MultiTexCoord0;
  34. gl_Position = ftransform();
  35. }

传入的lightPos,eyePos,gl_Vertex,gl_Normal,rm_Tangent是其模型坐标系下的坐标、向量,乘以
ModelView矩阵(法线切线乘以ModelView矩阵的转置逆矩阵)到了视图空间(vlightPos,veyePos,pos,norm,
vtangent);在视图空间它们已经有了“世界”的概念了,因此可以平等地相互影响(在各自封闭的模型空间是享受不了的),可以作各种点乘叉乘加减乘
除计算。

注意,lightPos,eyePos虽说是在其各自模型坐标系下定义的,但不对它们弄什么平移旋转缩放操作的话,其模型矩阵就是一单位阵,此时其“世界坐标
==
模型坐标”。所以这时我可以当它是在世界空间定义的坐标(实际上一般我们都会在世界空间定义这两个点)。(注意,前提是不对它们做模型变换。)

从以上量得到光源向量、视线向量后(它们在视图空间),N、T叉乘得B(注意它们现在都在视图空间),通过TBN矩阵逆矩阵把两向量变换到当前顶点的切线空间,交给光栅去插值。

对以上有不理解的朋友,可能是没看上篇:[shader复习与深入:Normal
Map(法线贴图)Ⅰ
]

fragment
shader:

  1. //fragment shader
  2. uniform float shiness;
  3. uniform vec4 ambient, diffuse, specular;
  4. uniform sampler2D bumptex;
  5. uniform sampler2D basetex;
  6. float amb = 0.2;
  7. float diff = 0.2;
  8. float spec = 0.6;
  9. varying vec3 lightdir;
  10. varying vec3 halfvec;
  11. varying vec3 norm;
  12. varying vec3 eyedir;
  13. void main(void)
  14. {
  15. vec3 vlightdir = normalize(lightdir);
  16. vec3 veyedir = normalize(eyedir);
  17. vec3 vnorm =   normalize(norm);
  18. vec3 vhalfvec =  normalize(halfvec);
  19. vec4 baseCol = texture2D(basetex, gl_TexCoord[0].xy);
  20. //Normal Map里的像素normal定义于该像素的切线空间
  21. vec3 tbnnorm = texture2D(bumptex, gl_TexCoord[0].xy).xyz;
  22. tbnnorm = normalize((tbnnorm  - vec3(0.5))* 2.0);
  23. float diffusefract =  max( dot(lightdir,tbnnorm) , 0.0);
  24. float specularfract = max( dot(vhalfvec,tbnnorm) , 0.0);
  25. if(specularfract > 0.0){
  26. specularfract = pow(specularfract, shiness);
  27. }
  28. gl_FragColor = vec4(amb * ambient.xyz * baseCol.xyz
  29. + diff * diffuse.xyz * diffusefract * baseCol.xyz
  30. + spec * specular.xyz * specularfract ,1.0);
  31. }

注意把normal map里的normal由(0,1)映射回(-1,1)。baseCol得到的是基底纹理的像素颜色。其余部分就是per
pixel lighting的东西了。

NormalMap 贴图 [转]的更多相关文章

  1. NormalMap 贴图 【转】

    转载: http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map.html   说起Normal Map(法线贴图),就 ...

  2. SpriteBuilder实现2D精灵光影明暗反射效果(一)

    其实不用3D建模,用2D的图像就可以模拟3D场景中光照反射的效果. 这里我们不得不提到一个normalMap(法线图)的概念,请各位童鞋自己度娘吧,简单来说它可以使得2D表面生成一定细节程度的光照方向 ...

  3. NormalMap 法线贴图

    法线贴图+纹理贴图(细节明显) 纹理贴图 法线贴图 法线贴图 存储法线的一张贴图,归一化的法线的 xyz 的值被映射成为对应的 RGB 值.归一化的法线值为[-1,1],RGB的每一个分量为无符号的8 ...

  4. Esfog_UnityShader教程_NormalMap法线贴图

    咳咳,好久没有更新了,一来是这段时间很忙很忙,再来就是自己有些懒了,这个要不得啊,赶紧补上.在前面我们已经介绍过了漫反射和镜面反射,这两个是基本的光照类型,仅仅依靠它们就想制作出精美的效果是远远不够的 ...

  5. Unity3d《Shader篇》法线贴图

    效果图 贴图 法线贴图 //代码 Shader "Custom/NormalMap" { Properties { _MainTex ("Texture", 2 ...

  6. 在 OpenGL ES 2.0 上实现视差贴图(Parallax Mapping)

    在 OpenGL ES 2.0 上实现视差贴图(Parallax Mapping) 视差贴图 最近一直在研究如何在我的 iPad 2(只支持 OpenGL ES 2.0, 不支持 3.0) 上实现 视 ...

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

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

  8. Shader中贴图知识汇总: 漫反射贴图、凹凸贴图、高光贴图、 AO贴图、环境贴图、 光照纹理及细节贴图

    原文过于冗余,精读后做了部分简化与测试实践,原文地址:http://www.j2megame.com/html/xwzx/ty/2571.html   http://www.cnblogs.com/z ...

  9. Unity3D ShaderLab法线贴图

    Unity3D ShaderLab法线贴图 说到法线贴图,应该算是我们最常使用的一种增强视觉效果的贴图.将法线贴图的各个像素点座位模型的法线,这样我们的光照可以模拟出高分辨率的效果, 同时也保持较低的 ...

随机推荐

  1. ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: Incompatible namespaceIDs

    用三台centos操作系统的机器搭建了一个hadoop的分布式集群.启动服务后失败,查看datanode的日志,提示错误:ERROR org.apache.hadoop.hdfs.server.dat ...

  2. AngularJs 特性 之 双向数据绑定

    <!DOCTYPE html> <html lang="en" ng-app> <head> <meta charset="UT ...

  3. SPOJ 10628 Count on a tree(Tarjan离线 | RMQ-ST在线求LCA+主席树求树上第K小)

    COT - Count on a tree #tree You are given a tree with N nodes.The tree nodes are numbered from 1 to  ...

  4. BZOJ2396 神奇的矩阵 【随机化 + 矩乘】

    题目链接 BZOJ2396 题解 一种快速判断两个矩阵是否相等的方法: 对于两个\(n * n\)矩阵,两边同时乘一个\(n * 1\)的随机矩阵,如果结果相等,那么有很大概率两个矩阵相等 如果左边是 ...

  5. jQuery初级

    一.简介 定义 jQuery创始人是美国John Resig,是优秀的Javascript框架: jQuery是一个轻量级.快速简洁的javaScript库.源码戳这 jQuery对象 jQuery产 ...

  6. php函数注释

    https://segmentfault.com/q/1010000003087072 phpstorm /**+回车

  7. 十步优化SQL Server中的数据访问

    原文发布时间为:2011-02-24 -- 来源于本人的百度文章 [由搬家工具导入] 转载:http://tech.it168.com/a2009/1125/814/000000814758_all. ...

  8. ASP.NET三层架构的优点和缺点

    原文发布时间为:2009-10-24 -- 来源于本人的百度文章 [由搬家工具导入] 小项目,以后变动不大的不用三层架构。 ASP.NET三层结构说明 完善的三层结构的要求是:修改表现层而不用修改逻辑 ...

  9. asp.net几种开源上传控件,flash,ajax版,支持多文件

    原文发布时间为:2010-03-18 -- 来源于本人的百度文章 [由搬家工具导入] 1、AspnetUpload 地址:http://www.aspnetupload.net/ 最早接触的上传控件。 ...

  10. hdu 3980 Paint Chain 组合游戏 SG函数

    题目链接 题意 有一个\(n\)个珠子的环,两人轮流给环上的珠子涂色.规定每次涂色必须涂连续的\(m\)颗珠子,无法继续操作的人输.问先手能否赢. 思路 参考 转化 第一个人取完之后就变成了一条链,现 ...