openGL CG 系列教程06 – Normal Mapping (法线贴图)

原创 2010年05月13日 19:45:00

*原创文章,转载请注明出处*

 

openGL CG 系列教程06 – Normal Mapping (法线贴图)

 

Normal Mapping(法线贴图),不论是在游戏开发还是其他计算机图形开发中都是使用很广泛的技术。如果一个物体的表面粗糙不平,物体顶点的法线也就朝向各个不同的方向,所以物体看起凹凸不平。要表现这样的物体,当然可以使用相当多的包含不同法线的顶点数据,这样做的效率可想而知是很低的。在要求及时性很高的交互式图形程序,比如游戏中,显然这种方法不适用。为了解决这个问题,提出了法线贴图的概念。

 

法线贴图是一张普通的纹理贴图,但是和一般纹理贴图不一样的是,法线贴图中的每个像素保存的是法线数据。一般通过高度图来生成法线贴图。高度图是8位灰度图,颜色越深代表高度越低,颜色越浅代表高度越高。如图Fig1(a)就是用photoshop生成的一张高度图。在图Fig1中可以看到,从高度图(a)生成法线图(b)然后贴在物体表面时(c),浅色的部分就表示物体表面“凸”的部分,高度图中颜色深的部分就表示物体表面“凹”的部分。

 

(a).高度图

(b).法线图

(c).应用到物体表面

Fig1. 高度图和法线图

 

先来看看如何从高度生成法线图。利用高度图中的数据,计算两个差向量(1, 0, Hr – Hg)(0, 1, Ha – Hg),而法线就等于它们的外积。其中Hg是当前像素,Ha是当前像素上面的一个像素,Hr是当前像素的右面的一个像素。从下面的图Fig2可以清楚的看出它们的关系。

 

 

Ha

 

 

Hg

Hr

 

 

 

Fig2. HgHaHr

 

计算出两个差向量的外积并单位化,就可以的到法线计算的公式。

 

noraml

 

要注意的是,法线贴图时保存在纹理数据里然后传到shader里的,一般纹理数据保存的数据类型是无符号的整型,范围是0255,或者用无符号浮点数01表示。而我们计算出来的法线是带符号的浮点类型数据,所以这样要把计算出来的法线转换为01之间的无符号浮点数据类型。由于单位化后的法线各分量的范围是-11的浮点数,那么将法线转换为颜色可以表示为

 

Color = Normal × 0.5 + 0.5

 

如果使用的GPU支持带符号的浮点数类型的话,也可以不做这样的转换。如果GPU不支持,则需要转换后再传入到shader,在shader中使用的时候不要忘了把颜色再次转换到-11之间的法线。

 

Normal = 2 × (Color - 0.5)

 

HeightMap2Normal.cpp

IplImage *pImg = cvLoadImage("HeightMap.bmp");  //从文件读取高度图

      int q = pImg->nChannels;

 

      CvSize csize;

      csize.width = 128;

      csize.height = 128;

 

      IplImage *nP = cvCreateImage(csize, 8, 3);  //创建一个带有RBG三个通道的深度为8的法线图

 

      CvScalar s;

      CvScalar r;

      for(int i = 0;i<128;i++)

      {

           for(int j = 0 ;j<128;j++)

           {

                int Hg,Ha,Hr;

                s=cvGet2D(pImg,i,j);

                 Hg = s.val[0];

 

                 if(i-1<0)   Ha = 0;

                 else

                 {

                      s = cvGet2D(pImg,i-1,j);

                      Ha = s.val[0];

                 }

              

                 if(j+1>127)   Hr = 0;

                 else

                 {

                      s = cvGet2D(pImg,i,j+1);

                      Hr = s.val[0];

                 }

 

                 r = cvGet2D(nP, i, j);

                 r.val[2] = (Hg-Ha)*0.5+128;     // 为了保存法线图为位图,这里将法线转换为[0,255]之间的整数

                 r.val[1] = (Hg-Hr)*0.5+128;

                 r.val[0] = 255;

                 cvSet2D(nP,i,j,r);

           }

      }

 

上面这段代码将高度图转换为法线贴图,为了简便,使用了OpenCV。由于高度图是灰度图,RGB各分量的值都相等,所以代码中只适用了0通道。OpenCV默认bmp图片保存方式为BGR,最后计算出的法线各分量的值对应的顺序也要改变。Fig1中的图(b)就是从高度图(a)中计算出法线后,转换为颜色值保存到bmp图中的结果。另外在计算法线的时候,如果在公式分子法线向量的xy分量乘以大于0的常数,那么将得到更加分明的凹凸效果,如图Fig3

 

(a).乘以常数1

(b).乘以常数5

Fig3. 法线贴图的比较

 

生成好法线贴图后,就可以传入shader中使用了。法线贴图中的法线信息是每个像素的法线,因此就要像pixel lighting一样在fragment shader中将计算光照所要用的法线从法线贴图中取出即可。

 

06vs.cg

void vs_main(float4 position : POSITION,

                  float2 texCoord : TEXCOORD0, //法线贴图的纹理坐标

                  

                  out float4 oPosition      : POSITION,

                  out float2 oTexCoord      : TEXCOORD0,

                  out float3 objPos         : TEXCOORD1,      // 物体本地坐标系中的顶点坐标

 

                  uniform float4x4 MVP)

{

 

      oPosition = mul(MVP, position);

      oTexCoord = texCoord;

      objPos = position.xyz;

 

}

 

这段vertex shader很简单,主要是将法线贴图的纹理坐标和物体的个顶点坐标传入fragment shader中。下面是fragment shader的代码。

 

06fs.cg

float3 expand(float3 v)

{

      return (v-0.5) * 2.0;

}

 

void fs_main(float2 normalMapCoord  : TEXCOORD0,

                  float3 objPos          : TEXCOORD1,

                

                  out float4 color : COLOR,

 

                  uniform float3 lightPosition,

                  uniform float3 eyePosition,

                  uniform float4 LMd,

                  uniform float4 LMs,

                  uniform sampler2D normalMap)

{

     

      float3 normalTex = tex2D(normalMap, normalMapCoord).xyz;

      float3 normal = expand(normalTex);

 

      float3 lightDir = normalize( lightPosition-objPos );

      float3 eyeDir   = normalize( eyePosition - objPos );

 

      float3 H = normalize( lightDir + eyeDir );

 

      float diffuse = saturate(dot(normal, lightDir));

      float specular = saturate(dot(normal, H));

 

      color.xyz = LMd * diffuse + LMs*pow(specular, 64);

      color.w = 1.0;

 

}

 

可以看到fragment shader代码和前面pixel lighting中计算光照差不多。这里增加了一个函数

 

float3 expand(float3 v)

{

      return (v-0.5) * 2.0;

}

 

该函数的功能就是前面所讲到的,法线数据被转换为纹理颜色传入shader,使用的时候就要将它再次还原为法线数据。fragment shader中的传入参数多了个uniform sampler2D normalMapfloat2 normalMapCoord,分别就是法线贴图和纹理坐标,然后通过纹理查询函数tex2D()就可以得到每个像素对应的法线数据,然后将此法线作为后面光照计算所用的法线即可。如果将这段shader应用到一个在xy平面上的四边形,就可以得到Fig4的效果。

 

(a)

(b)

(c)

Fig4 法线贴图应用到xy平面的四边形

 

从上面的图中可以看到光源移动后所显示出的不同,并且结果是正确的。现在如果地面要使用法线贴图来显示地面的凹凸不平,我们试着将这个法线贴图用到xz平面上。

 

(a)

(b)

Fig5 不同平面应用法线贴图的效果

 

应用到xz平面上我们发现结果不正确,从Fig5(a)中可以看到,红色的点是光源的位置,如果光源在地面的上方,地面后半部分应该也被光照亮,而不应该是黑色的。图(a)中地面有一片黑色的部分,很明显这里效果不正确。我们希望得到图(b)的效果,下面来看看哪里出了问题。

 

到现在为止所以针对法线的计算,都是基于物体本地坐标系的。从高度图开始,由于图片是xy平面上的像素组成的,生成法线图的时候当然也是在这个本地坐标系。所以我们将法线图应用于xy平面上的物体物体是效果是正确的。一旦对象的本地坐标系发生改变,再用本地坐标系改变前所生成的法线贴图效果就不正确了,就像我们从Fig5(a)中看到的一样。要得到正确的效果,可以再坐标系发生变换后,重新生成法线图。很明显这样做效率太低,如果应用法线贴图的物体是运动的,那么每一帧都要重新生成法线贴图。为了解决这个问题,我们使用贴图坐标系(Texture Coordinate)

 

对每个顶点分配纹理坐标的时候,将每个顶点看成坐标原点,然后分别使用该顶点的切线T (Tangent),第二法线B (Binormal)和法线N (Normal)构成一个坐标系。在几何上,由T B N组成的坐标系称为Frenet Frame。于是这个贴图坐标系F就可以表示为

 

F = [ T  B  N ]

 

只要知道F其中任意两个分量,都可以求出第三个分量,它们之间有这样的关系

 

T = B × N

B = N × T

N = T × B

 

为了得到正确的效果,之前shader中的代码就在做部分修改,为了构成贴图坐标系,就要将法线和切线传入shader中。

 

06vs.cg

void vs_main(float4 position : POSITION,

                  float2 texCoord : TEXCOORD0,

                  float3 normal   : NORMAL,

                  float3 tangent  : TEXCOORD2,

                  

                  out float4 oPosition      : POSITION,

                  out float2 oTexCoord      : TEXCOORD0, // textrue coordinate

                  out float3 oNormal        : TEXCOORD1, // normal vector

                  out float3 oTangent       : TEXCOORD2, // tangent vector

                  out float3 objPos         : TEXCOORD3,      // object space vetex

 

                  uniform float4x4 MVP)

{

 

      oPosition = mul(MVP, position);

 

      oTexCoord = texCoord;

      oNormal = normal;

      oTangent = tangent;

      objPos = position.xyz;

 

}

 

现在vertex shader中多传入了两个参数,tangentnormal,然后将它们传入fragment shader,在fragment shader中,将光照计算要用到的所有向量都转换到贴图坐标系中。于是之前的fragment shader只需要做以下的修改即可。

 

float3 T = tangent;

float3 N = normal;

float3 B = cross(N,T);

 

float3x3 M = float3x3(T,B,N);

 

float3 lightDir = normalize( mul(M, lightPosition-objPos) );

float3 eyeDir   = normalize( mul(M, eyePosition - objPos) );

 

现在shader代码已经修改,我们再次应用到程序中,这次我们增加一个墙面,使用2个墙面和一个地面,然后再来看看效果。

 

Fig6 法线贴图用于不同平面

 

Fig7 法线贴图于圆环

 

Fig6Fig7中可以看出,利用贴图坐标系的法线贴图可以应用于各种几何体。最后想说的一点的是,整个法线贴图的过程都是基于fragment shader的,因为我们要计算每个像素的颜色。于是在fragment shader中要涉及到单位化向量的操作,有的GPU并不支持在fragment shader使用normalize函数。于是有一种更快速更普遍的方法,那就是使用Normalization Cube Map,该方法是向fragment shader再传入一个cube map,这样就不必向fragment shader传入光源位置和相机位置来计算光照需要的方向向量了,而是可以在vertex shader中计算每个顶点的关于光照计算所需要的方向向量,然后传入fragment shader,利用函数texCUBE即可得到每个像素单位化后的相关向量。具体的实现方法大家可以google一下。

 

 

 

 

 *原创文章,转载请注明出处*

OpenGL核心技术之法线贴图

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等...
  • jxw167
  • jxw167
  • 2017年02月28日 16:16
  • 1632

OpenGL3.0教程 第十三课:法线贴图

OpenGL3.0教程 原文链接:http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-12-opengl-extensi...

OpenGL 法线贴图 切线空间 整理

Parallax Mapping Bump Map通过对一个平面上的像素点上增加法向量信息,因此通过像素级的光照后,平面上就具有凹凸的表现,但他仍然是一个平面,呈现不出几何上的高低。而Parall...

openGL CG 系列教程4 - Lighting + Texture

*原创文章,转载请注明出处*openGL CG 系列教程04 – Lighting + Texture 前几个教程详细的讲解了使用可编程渲染管线实现如何实现光照。如果我们要渲染的模型是带有纹理,要想带...

openGL CG 系列教程2 - Vertex Lighting

*原创文章,转载请注明出处*openGL CG 系列教程2vertex lighting 之前的一篇教程HelloCG介绍了Cg的一些最基本的东西。这篇教程将介绍利用可编程渲染管线来实现光照。光照模型...

法线贴图normal mapping

  • 2015年04月03日 15:44
  • 423KB
  • 下载

Unity Shader-法线贴图(Normal)及其原理

简介 以前经常听说“模型不好看啊,怎么办啊?”答曰“加法线”,”做了个高模,准备烘一下法线贴图”,“有的美术特别屌,直接画法线贴图”.....法线贴图到底是个什么鬼,当年天真的我真的被这个图形学的...

对于法线贴图(Normal Map) 的深入研究

前几篇文章写过有关法线贴图的内容,这次文章将讨论其原理及相关优化。回过头来看一下原来的文章真有种想删掉的感觉。。。 为什么叫法线贴图,我们知道法线(Normal)是垂直于一个面的直线,通过计算光...

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

这篇文章写得非常好,学习了,转zi 自从看完那本《数学在计算机图形学上的应用》后,一直想好好地真正实践一次法线贴图/凹凸贴图呢(以前是根据橙书弄了一下罢了)。昨天偶尔看到篇涉及BumpMap的文...

Unreal 4 Double Normal Clear Coat 透明图层双法线贴图

透明图层双法线贴图可以实现更加复杂的材质效果。 材质球: 注意点: 1)需要设置Shading Model为Clear Coat (透明图层模式)。 2)需要拖入Cle...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:openGL CG 系列教程06 – Normal Mapping (法线贴图)
举报原因:
原因补充:

(最多只允许输入30个字)