• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

法线贴图和切线空间

武飞扬头像
离原春草
帮助1

法线贴图

法线贴图是一种技术,用专业一点的话来说,叫做采用低面数模型(低模)实现对高面数模型(高模)模拟的技术。用通俗的话来说,就是使用二维图形模拟三维效果的技术。譬如,在一面从模型上来看是平滑的墙上看到粗糙的质地的三维效果(所谓的凹凸贴图),这种粗糙质地表现为,随着光照方向的变化,阴影也会跟着变化,类似自然界中物体表面不平整的凹凸效果。如下图所示,左边的模型与右边的模型分别为低模与高模,法线贴图的作用就是想使用左边的模型得到右边模型的效果,从而保证了绘制的效率与精度。

我们知道,模型表面上像素的光照效果(包括颜色、阴影等)只与光照以及表面上的法线相关,在光照确定的情况下,则只由法线唯一约束,而通常所说的法线是没有长度的,只由方向唯一标识,故而,法线的方向决定了模型表面的光照效果。通常平滑表面的各个像素点的法线自然是完全一致的,导致光照射在其上的表现完全一致,呈现出平滑效果。而粗糙表面上各点法线各不相同,导致光照在其上的表现各不相同,从而呈现粗糙质地。

法线贴图(Normal Mapping)实质上是凹凸贴图(Bump Mapping)的一种。凹凸技术的目的是为了使平滑的表面有粗糙的质地感,其最初的手法是通过一张高度图记录各个像素点的高度信息,从而通过高度差得到各点的法线,而由于平面上各点有着高度差就会导致法线不同,从而根据光照表现与法线的关系得到凹凸效果。

凹凸贴图发展至今已经有了三十余年的历史,其中除了最早的高度图、目前广泛使用的法线贴图、还有一些其他的技术被提出并得到了相应的验证与推广,具体可以参照此文。今天要重点介绍的是法线贴图技术,刚才说到法线贴图是实质上是低模对高模的模拟,那么,这种模拟是怎样实现的呢?下面从原理上做一下简要介绍。

对于整体形状近似的低模与高模,例如上面图示中的两个模型,左边的低模看起来平滑,右边高模看起来粗糙,转换成相关术语则是,左边面数较少,右边面数较多。如此,对于左边的某个三角面片(以Triangle做陈述范例),那么在右边的模型中,可能对应多个三角面片,而右边模型越复杂,对应的面片数越多。如下图所示,将两个模型从二维转换成一维,那么低模就对应下面一条平滑曲线,而高模则对应上面插满了标枪的曲折曲线,将下方的图中两条曲线用折线近似模拟的话,下面曲线的每一段折线都将对应上面曲线的多条折线。而我们现如今想用下面一条曲线与光照配合来模拟出上面一条曲线的丰富细节,那么我们就应该要让下面曲线上各点的法线与上面曲线上各点法线相对应(至于点与点之间的对应方法,加入我们考虑最简单的,从上往下俯视,上下两条曲线每一对重合的点形成一个match),而之后,我们将上面一条曲线各个点的法线(也就是随地乱插的标枪)存储起来,在渲染下方曲线所表示的低模的时候,将法线一一对应到各点上,那么经过光照与法线的作用,在低模上各点就将呈现不同的风光,看起来就像那么回事了。关于法线贴图的一点具体资料可以参见此文

原理已经阐述清楚了(嗯,自认为阐述清楚了。。),关于法线贴图的具体实现,还有一些问题需要考虑:

  • 法线的获取
  • 法线的存储
  • 法线的应用

每个面法线的获取比较简单,取得这个面的三个顶点,之后构造出两条边向量,将二者叉乘即可,不过要注意保证方向,法线的方向总是指向上方。在这里有一点值得称道的是坐标系:

  • 实际上不管是左手坐标系,还是右手坐标系,对于两个代数表达式相同的向量,其叉乘的结果的代数表达也必然是相同的:不论左右手,这个结果都是正确的 cross((1,0,0), (0,1,0)) = (0,0,1)
  • 左手坐标系(如DX)的顶点环绕顺序是顺时针,与之相对应的是CCW(counter clock wise 逆时针)背面消隐,即在屏幕上顶点环绕顺序为逆时针的将被culling掉
  • 有手坐标系(如OpenGL,FBX,3ds Max)的顶点环绕顺序是逆时针,与之对应的是CW(顺时针)背面消隐,在屏幕上环绕顺序为顺时针的被忽略。

知道这点知识,就不会将法线方向搞反了。

法线的存储,这有什么好讲的呢,直接用三个浮点数组成的向量表示不就行了吗?没错,法线的确是用一个三维向量就可以表征,但是关于其具体的处理还有一些细节需要完善。

三维向量存在的基础是三维空间坐标系,在游戏开发中会涉及到众多的三维空间,世界空间坐标系,模型空间坐标系等,究竟应该选择什么坐标系作为法线向量的基础坐标系呢?显然,模型上各点法线是会随着模型的旋转而变化的,假如采用世界坐标系作为基础坐标系,那么法线向量将为固定的,一旦模型发生旋转变换,将会导致法线向量与真实的法线无法匹配(毕竟世界坐标系没变,原来存储的法线向量数值上就没有发生变化,但实际上的法线已经变了)。

那么,采用模型空间坐标系如何?这的确是一个可以实施的想法,将模型上各点相对于整个模型坐标系的法线向量保存下来,使用的时候通过矩阵变换将之转换到世界坐标系中,那么就解决了法线的存储以及模型变换的问题。但是,需求是永远在变化的,难缠的策划们脑洞大开的说,假如模型发生了变形要怎么办?很显然,如果模型发生了变形,举个例子,某个平面变成了拱形曲面,那么即使模型没有进行变换,但这个面上的法线还是发生了变化。天,世界上怎么有策划这种生物!

好在,上有政策,下有对策。考虑到变形的结果,也就是组成模型的各个三角面片进行了旋转等变换,虽然这些三角面片的位置以及朝向不同了,但是不变的是三角面片还依然是三角面片,组成三角面片的各个顶点也都还没变(嗯,顶点的坐标当然发生了变化,但是一个人不论处在北京还是东京,这个人还依然是这个人)。有人就想到,不如我们就对最小的单位——三角面片做处理,为每个三角面片设置一个坐标系,将法线保存在这个坐标系中怎么样?这样行么?当然行,而用这种方式实现的法线的存储方法就是传说中的切线空间,至于具体的实现原理,将在稍候送上。

法线贴图要考虑的最后一个问题是法线的应用,刚才说到了法线的存储,那最终我们计算各点光照的时候,要怎么讲存储起来的法线取出来呢?难道要读文件,用矩阵的方式按照行列索引取值?这种方式自然是可以的,不过为了更加直观的查看法线,前辈们采取了将法线存储为贴图的方式来解决这个问题。这就有趣了,法线怎么可以变成贴图呢?我们知道,法线有x,y,z三个分量(浮点数),而我们又知道一张二维图中的一个像素点的表现主要由颜色来控制,而颜色由RGB三原色控制,将xyz对应到RGB上,不就能够实现法线到贴图的转换了吗?也就是说,将一个模型上的所有点按照其上的贴图(texture)坐标,将法线转变为RGB表示的颜色值,并存储在贴图坐标上,形成一张贴图,称之为法线贴图。法线贴图,顾名思义,有法线,有贴图,贴图所指的就是这个意思了。

在这三点中,其中最为重要的是法线的存储,下面做一下重点介绍。

切线空间

法线的存储,不单是用什么样的格式来存储数据,更重要的是在存储之前要对数据进行怎样的处理。

对于以模型空间为基准的法线来说,其前期处理可以直接忽略,只需要面上各点的模型空间坐标计算得到即可。但是对于切线空间为基准的法线来说,这一步的处理就很重要了。

我们刚才说到,切线空间只是保存法线的一种手段。我们采用这种手段的终极目标就是要实现法线的存储,以期望当模型发生形变的时候,这个法线还依然能够用来计算光照。刚才又说到,在模型发生形变的时候,三角形面片的内部结构是固定的(基本可以假设固定),也就是各个像素的相对位置基本可以认为保持不变,所以我们才想到借用这些固定的数据来建立一个坐标系,实现法线的保存,为什么呢,因为模型发生形变,可以解释为组成模型的各个面发生旋转等变换,对应于一个变换矩阵。当模型发生形变的时候,我们的这个切线空间坐标系乘以这个面对应的变换矩阵就可以转变为模型坐标系中的数据,这样,再继续乘以模型坐标系到世界坐标系的变换矩阵,就能够变换为世界坐标系中的数据。

好了,原理基本上自以为介绍清楚了,那么有几个问题这里还需要详细考虑一下。

  • 切线空间坐标系要怎么选取并计算,也就是切线空间坐标系到模型坐标系的变换矩阵要怎么得到
  • 切线空间坐标系要怎么保存
  • 法线怎么基于切线空间进行保存
  • 一些善后的处理

首先,我们来看一下切线空间(TBN,Tangent, Binormal, Normal,切线空间由这三个向量组成)的计算方法,一般呢,我们是选择面法线作为切线空间的Z轴,而面上贴图的UV方向作为切线空间的XY轴,这里有人就会有疑问了,贴图的UV方向可不一定垂直呀?这样能作为坐标系的坐标轴么?没错,这个想法是正确的,UV的确是不一定垂直的,心灵手巧的美术同学们在分UV的时候,有时候就是需要使得贴图发生变形,从而导致UV并不垂直,但是这又如何?回忆一下课堂上老师对于代数几何的教学,使用标准正交基组成的坐标系叫做标准正交坐标系,而使用普通基组成的则是普通坐标系(咳咳,其实我也是刚刚去查资料才知道的。。)。使用普通坐标系,实际上是可以组成坐标系的,只要其中组成坐标系的各个向量是线性不相关的即可(当保证三个向量正交,则可以保证在切线空间中的两个向量当变换到模型坐标空间中,这两个向量的夹角将保持不变;另外,正交向量组成的正交矩阵,其逆矩阵就是其转置矩阵,因此可以简化很多计算。具体可以参见这篇文章)。另外,一般,我们的计算只需要使得各个向量为标准向量(即模为1)即可,对于方向是否正交,并没有严格要求,而通常,为了计算的方便,可以通过Gramm-Schmidt正交化方法或者其他正交化方法(如保证N不变,将B与N叉乘,得到T,再将N与T叉乘得到B,也能实现正交)完成正交。通过这个方法计算得到的TBN三个向量,即可组成一个3*3矩阵,这个矩阵,实质上就是从切线空间到模型坐标空间的变换矩阵,关于具体的计算,可以参考这篇文章还有这篇文章

第二个,说到切线空间坐标系的保存问题。我们知道以前的法线数据是保存在顶点中的,也就是将每个定点的法线数据保存在各自的数据中,而不保存面的法线数据,这是因为图形学中大部分的计算都是针对顶点展开的,而面上其他像素的计算结果都是通过对顶点计算结果的插值完成。所以,我们也需要将面上计算得到的TBN转化为顶点的TBN,这个转化就大有学问,对于由一个面独享的顶点,那没什么好说的,直接将面的TBN数据赋值给顶点即可(有的顶点虽然是被多个面共享,但是共享这个顶点的多个面不是属于同一光滑组,所以不需要对顶点数据进行平滑处理,也就是不需要对多个面的数据进行加权之后赋给顶点,而是将顶点拆分为多个顶点,每个面各抱一个回家,每个面抱回家的顶点的数据就对应于面的数据),其关键的问题在于由多个面(且这些面都是同一光滑组的)公用的顶点的TBN数据要怎么处理。Crytek引擎的计算时通过顶点所对应的夹角来完成平均的(请参见此文),而FBX(Autodesk的模型保存文件,具有保存切线空间数据的功能)计算很诡异,对于共用一条边的两个面来说,这条边对应有两个顶点,对于这两个顶点,FBX会将其拆分为四个顶点,每个顶点一分为二,各占五成,以AB来代替两面,对于A面的这两顶点来说,其TBN数据是TBN-A * 2 TBN-B,对于B面的两个顶点来说,其TBN数据为TBN-A TBN-B * 2,这种二比一的分配方式不知道是因为共用的是两个顶点还是因为其他原因。对于虚幻引擎,其对于面数据到顶点数据的处理就更为复杂,以下用代码代替,有兴趣的同学们可以自行发掘探索。


//////////////////////////////////////////////////////////
// 功能:仿照虚幻引擎的切线空间处理方式,根据平滑组对切线空间数据进行平滑处理
// 参数:Mesh &mesh    网格数据
// 参数:vector<D3DXVECTOR3> &Tan    切线数据
// 参数:vector<D3DXVECTOR3> &Binor 副法线数据
// 参数:vector<D3DXVECTOR3> Nor        法线数据
// 参数:Index   对max的顶点环绕方向进行修正的索引数组
// 返回:无
//////////////////////////////////////////////////////////
void TangentSpaceSmooth4VE (Mesh & mesh, vector <D3DXVECTOR3> & Tan, vector <D3DXVECTOR3> &Binor, vector <D3DXVECTOR3> Nor, const int * const Index, BOOL vertexOrthogonal )
{
                 int numTris = mesh.getNumFaces();  //面数
                 Face *geomFaces = mesh.faces;                        //面数据
                 TVFace *mapFaces = mesh.tvFace;                  //贴图数据
                 Point3 *geomVerts = mesh.verts;                      //顶点数据
                 D3DXVECTOR3 zeroVec (0, 0, 0);                      
                 vector<int > adjacentFaces;
                 vector<float > determinant( numTris, 0.f);
                
                 //计算各个面的切线空间的行列式
                 for (int i = 0; i < numTris ; i  )
                {
                                 determinant[i ] = computeHybridProd( Tan[i * 3], Binor[i * 3], Nor[i * 3]);
                }

                 //用于存储与某个面的共用某个顶点的邻面的相关信息
                 struct FFanFace
                {
                                 int faceIndex ;                         //邻面索引
                                 int linkedVertexIndex ;           //共用顶点在邻面的位置(未经过环绕方向Index处理)
                                 bool bFilled ;           //判断是否处于同一光滑组,且可用于对切线空间向量进行光滑
                                 bool bBlendNormals ;           //法线是否满足光滑条件
                                 bool bBlendTangents ; //切线是否满足光滑条件
                };

                 //暂存中间数据
                 vector<D3DXVECTOR3 > norCopy( Nor);
                 vector<D3DXVECTOR3 > tanCopy( Tan);
                 vector<D3DXVECTOR3 > binorCopy( Binor);
                
                 //计算需要使用到的存储列表
                 vector<FFanFace > relateFace4Ver[3];               //存储与某个顶点相关的面索引
                 vector<int >            dupVerts;                               //存储与某个顶点索引相同的顶点位序


                 for (int fInd = 0; fInd < numTris ; fInd  )
                {
                                 int faceOffset = fInd * 3;
                                 Face& geomFace = geomFaces[ fInd];
                                 Point3 vPos [3];
                                 D3DXVECTOR3 tanArr [3], binorArr[3], norArr[3];

                                 for (int vInd = 0; vInd < 3; vInd   )
                                {
                                                 vPos[vInd ] = geomVerts[ geomFaces[fInd ].v[vInd]];

                                                 tanArr[vInd ] = zeroVec;
                                                 binorArr[vInd ] = zeroVec;
                                                 norArr[vInd ] = zeroVec;

                                                 relateFace4Ver[vInd ].clear();
                                }

                                 //对于至少两顶点重合的退化三角形,将其数据设置为0
                                 if (vPos [0] == vPos[1] || vPos[1] == vPos [2] || vPos[0] == vPos[2])
                                {
                                                 for (int vInd = 0; vInd < 3; vInd   )
                                                {
                                                                 Tan[faceOffset   vInd] = zeroVec;
                                                                 Binor[faceOffset   vInd] = zeroVec;
                                                                 Nor[faceOffset   vInd] = zeroVec;
                                                }
                                                 continue;
                                }

                                 //查找与当前面具有共同顶点的面,将其索引存入AdjacentFaces中
                                 adjacentFaces.clear ();
                                 for (int vInd = 0; vInd < 3; vInd   )
                                {
                                                 int vertexIndex = geomFace. v[vInd ];
                                                 dupVerts.clear ();
                                                 for (int i = 0; i < numTris ; i  )
                                                                 for (int j = 0; j < 3; j   )
                                                                {
                                                                                 //查找具有相同索引的顶点,其中自身也被当成一份子算入
                                                                                 if (geomFaces [i].v[j] == vertexIndex)
                                                                                                 dupVerts.push_back (i * 3   j);
                                                                }

                                                 int dSize = dupVerts. size();
                                                 for (int i = 0; i < dSize ; i  )
                                                                 adjacentFaces.push_back (dupVerts[ i] / 3);//应该要保持unique
                                }

                                 //去除重复的面
                                 sort(adjacentFaces .begin(), adjacentFaces.end ());
                                 adjacentFaces.erase (unique( adjacentFaces.begin (), adjacentFaces. end()), adjacentFaces.end ());

                                 //对与当前面相邻的面进行处理,根据共用顶点不同,将信息存入不同的vector中
                                 for (int ajctFace = 0; ajctFace < adjacentFaces .size(); ajctFace  )
                                {
                                                 int adjFaceIndex = adjacentFaces[ ajctFace];

                                                 //对此面的每个顶点,找到与之相关的每个邻面,并进行信息搜集处理(共用了几个顶点,是否可以参与向量光滑)
                                                 for (int vInd = 0; vInd < 3; vInd   )
                                                {
                                                                 FFanFace newFanFace ;
                                                                 int commonIndexCount = 0;

                                                                 //将相邻面的公共顶点存入一个FFanFace结构中linkedVertexIndex中,对于两个面中的每一对公共顶点,都对应一个FFanFace结构
                                                                 if (fInd == adjFaceIndex)
                                                                {
                                                                                 commonIndexCount = 3;
                                                                                 newFanFace.linkedVertexIndex = vInd;
                                                                }
                                                                 else
                                                                {
                                                                                 for (int adjVerIndex = 0; adjVerIndex < 3; adjVerIndex   )
                                                                                {
                                                                                                 if (geomVerts [geomFaces[ fInd].v [vInd]] == geomVerts[geomFaces [adjFaceIndex]. v[adjVerIndex ]])
                                                                                                {
                                                                                                                 commonIndexCount  ;
                                                                                                                 newFanFace.linkedVertexIndex = adjVerIndex;
                                                                                                                 break;//对于某个顶点而言,不可能出现与两个顶点重合的情况(如果出现,则为退化情况,之前已经处理过)
                                                                                                }
                                                                                }

                                                                }

                                                                 //如果有共用顶点,则将相关信息填充后,存入与顶点对应的vector中
                                                                 if (commonIndexCount > 0)
                                                                {
                                                                                 newFanFace.faceIndex = adjFaceIndex;
                                                                                 newFanFace.bFilled = (adjFaceIndex == fInd);                //最开始,只有此三角面自身可以参与自身三个顶点的向量光滑
                                                                                 newFanFace.bBlendNormals = newFanFace. bFilled;
                                                                                 newFanFace.bBlendTangents = newFanFace. bFilled;
                                                                                 relateFace4Ver[vInd ].push_back( newFanFace);
                                                                }
                                                }
                                }

                                 //再进行一轮处理,在此处理中,光滑组信息会被考虑,处理完成后,法线与切线的平滑条件被修改,得到能够参与到平滑过程中的所有面的相关信息
                                 for (int vInd = 0; vInd < 3; vInd   )
                                {
                                                 int relVecSize = relateFace4Ver[ vInd].size ();
                                                 int newConnections ;//循环条件,大于0,则继续循环。 其意义为,还存在着从bFilled的false阵营向true阵营的渗透
                                                 do
                                                {
                                                                 newConnections = 0;
                                                                 for (int curFaceIndex = 0; curFaceIndex < relVecSize ; curFaceIndex  )
                                                                {
                                                                                 FFanFace &curFace = relateFace4Ver[vInd ][curFaceIndex]; //取出当前待考量三角面的邻接信息
                                                                                 if (curFace .bFilled) //最初,只有最原始的三角面本身满足此条件
                                                                                {
                                                                                                 for (int nextFaceIndex = 0; nextFaceIndex < relVecSize ; nextFaceIndex  )
                                                                                                {
                                                                                                                 FFanFace &nextFace = relateFace4Ver[vInd ][nextFaceIndex];
                                                                                                                 if (nextFace .bFilled) //将所有的相关面片分割成两个部分,以bFilled(是否同阵营)标记,以true阵营为基础,不断腐蚀false阵营
                                                                                                                                 continue;

                                                                                                                 //如果不属于一个光滑组,则不需处理,即不必要参与到光滑中
                                                                                                                 if (mesh.faces [curFaceIndex]. smGroup&mesh .faces[nextFaceIndex]. smGroup) // || curFaceIndex == nextFaceIndex
                                                                                                                                 continue;

                                                                                                                 int commonVertices = 0;
                                                                                                                 int commonNormals = 0;
                                                                                                                 int commonTangents = 0;

                                                                                                                 //根据相应的判断条件,对是否需要进行法线、切线光滑进行设置
                                                                                                                 for (int curVertIndex = 0; curVertIndex < 3; curVertIndex   )
                                                                                                                {
                                                                                                                                 for (int nextVertIndex = 0; nextVertIndex < 3; nextVertIndex   )
                                                                                                                                {
                                                                                                                                                 if (geomVerts[geomFaces [curFaceIndex]. v[curVertIndex ]] == geomVerts[geomFaces [nextFaceIndex]. v[nextVertIndex ]])
                                                                                                                                                {
                                                                                                                                                                commonVertices   ;
                                                                                                                                                                                
                                                                                                                                                                //在顶点坐标相同的情况下,判断UV坐标是否相等,来确定是否进行切线空间平滑
                                                                                                                                                                if (mesh.getTVert (mapFaces[ curFaceIndex].t [curVertIndex]) == mesh.getTVert (mapFaces[ nextFaceIndex].t [nextVertIndex]))
                                                                                                                                                                {
                                                                                                                                                                                 commonTangents  ;
                                                                                                                                                                }

                                                                                                                                                                //根据索引是否相同,来确定是否需要进行法线光滑
                                                                                                                                                                if (geomFaces[curFaceIndex ].v[curVertIndex] == geomFaces[nextFaceIndex ].v[nextVertIndex]) // || bBlendOverlappingNormals
                                                                                                                                                                {
                                                                                                                                                                                 commonNormals  ;
                                                                                                                                                                }
                                                                                                                                                }
                                                                                                                                }
                                                                                                                }

                                                                                                                 //当两个面至少共用一条边的时候,可以考虑对数据进行平滑处理
                                                                                                                 if (commonVertices > 1)
                                                                                                                {
                                                                                                                                 newConnections  ;
                                                                                                                                 nextFace.bFilled = true;        //加入true阵营:同一光滑组,且至少与true阵营中某面共边
                                                                                                                                 nextFace.bBlendNormals = (commonNormals > 1);        //平滑法线的条件是,至少有两个共同顶点满足法线平滑的条件,即具有相同索引
                                                                                                                                 if (curFace .bBlendTangents && commonTangents > 1) //平滑此面切线的基本条件是,此面的入党介绍人满足切线平滑条件,且此面至少有两个公共顶点具有相同的UV坐标
                                                                                                                                {
                                                                                                                                                 //切线平滑升级条件:此面切线空间与最原始面的切线空间具有一致的方向(即满足相同的手系?)
                                                                                                                                                 if (determinant [fInd] * determinant[nextFaceIndex ] > 0.0f)
                                                                                                                                                                nextFace .bBlendTangents = true;
                                                                                                                                }
                                                                                                                }
                                                                                                }
                                                                                }
                                                                }
                                                } while (newConnections > 0);
                                }

                                 //顶点数据平滑处理
                                 for (int vInd = 0; vInd < 3; vInd   )
                                {
                                                 int relVecSize = relateFace4Ver[ vInd].size ();
                                                 for (int relFaceIndex = 0; relFaceIndex < relVecSize ; relFaceIndex  )
                                                {
                                                                 FFanFace const & relateFace = relateFace4Ver[vInd ][relFaceIndex];

                                                                 //不属于同一光滑组,或者没有与原始面片共边
                                                                 if (!relateFace .bFilled)
                                                                                 continue;

                                                                
                                                                 int relateFaceIndex = relateFace. faceIndex;
                                                                 if (relateFace .bBlendNormals)
                                                                {
                                                                                 norArr[vInd ]  = norCopy[ relateFaceIndex * 3];
                                                                }
                                                                 if (relateFace .bBlendTangents)
                                                                {
                                                                                 tanArr[vInd ]  = tanCopy[ relateFaceIndex * 3];
                                                                                 binorArr[vInd ]  = binorCopy[ relateFaceIndex * 3];
                                                                }
                                                }
                                }

                                 //对顶点切线空间数据进行正交化处理,然后将之存入相应的vector中
                                 for (int vInd = 0; vInd < 3; vInd   )
                                {
                                                 if (vertexOrthogonal )
                                                {
                                                                 //先归一化
                                                                 D3DXVec3Normalize(&tanArr [vInd], & tanArr[vInd ]);
                                                                 D3DXVec3Normalize(&binorArr [vInd], & binorArr[vInd ]);
                                                                 D3DXVec3Normalize(&norArr [vInd], & norArr[vInd ]);
                
                                                                 //斯密特正交(与虚幻完全一致)
                                                                 binorArr[vInd ] = binorArr[ vInd] - D3DXVec3Dot (&tanArr[ vInd], &binorArr[vInd ]) * tanArr[ vInd];
                                                                 D3DXVec3Normalize(&binorArr [vInd], & binorArr[vInd ]);
                                                                 tanArr[vInd ] = tanArr[ vInd] - D3DXVec3Dot (&norArr[ vInd], &tanArr[vInd ]) * norArr[ vInd];
                                                                 D3DXVec3Normalize(&tanArr [vInd], & tanArr[vInd ]);
                                                                 binorArr[vInd ] = binorArr[ vInd] - D3DXVec3Dot (&norArr[ vInd], &binorArr[vInd ]) * norArr[ vInd];
                                                                 D3DXVec3Normalize(&binorArr [vInd], & binorArr[vInd ]);
                                                }

                                                 //需要加上Index进行左右手坐标系的交换
                                                 Tan[faceOffset   Index[vInd]] = tanArr[vInd ];
                                                 Binor[faceOffset   Index[vInd]] = binorArr[vInd ];
                                                 Nor[faceOffset   Index[vInd]] = norArr[vInd ];
                                }
                }

}

第三个,说到了顶点法线的在切线空间中的保存。我们知道,我们要保存的实际上是高模上各点的法线,而法线的存储是借助低模的面上的切线空间完成的。也就是,我们计算低模上各个面的切线空间,并将高模上各个点的法线(模型空间)经过空间变换,转换为切线空间中的法线数据,并以贴图形式保存下来(由于对于低模的一个面上各点对应的高模上各点的法线与低模上面法线基本平行,导致转变为切线空间中各个法线基本上与Z轴平行,而Z轴对应于贴图中的B分量,所以就导致贴图整体呈现蓝色)。在最后进行图形渲染的时候,将从法线贴图上取出的切线空间法线与模型上各面的切线空间相乘,就可以得到模型空间上的法线,而当物体发生形变,就会导致模型空间的面发生变换,继而导致切线空间的变换矩阵发生变化,这时候,只需要乘上变化后的切线空间矩阵,就可以保证变形后的光照效果接近自然界中真实变形的情况,而如果不重新计算各个面的切线空间矩阵,而是沿用最开始的切线空间矩阵,所得到的效果就跟使用普通法线贴图(而非切线空间法线贴图)一致。而为了保证显示效果,一般需要保证计算法线贴图的切线空间坐标系与最终渲染时候用的切线空间坐标系一致。

最后,关于切线空间的实现还有一些善后处理,比如:

  • 贴图翻转,导致法线反向
  • 镜像贴图,导致法线反向
  • 柱面贴图,导致边缘重合,进而使得贴图坐标发现错误
  • 球面贴图等

关于这些问题的处理,在这篇文章中有详细介绍,有兴趣的同学们请自行翻阅。

总结

关于法线贴图与切线空间就讲到这里,下面看下采用这种方式得到的模型在光照变化下的凹凸效果。图像右边的为光照方向的改变,光源起点在圆心。

关于切线空间以及法线贴图,上面文章中介绍的也都只是很少的一部分,还有很多的知识等待发掘,如果发现文中有不恰当或者不正确的介绍,请大家不吝指正。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhhageak
系列文章
更多 icon
同类精品
更多 icon
继续加载