这是我以前在其它地方写的, 转到这里来, 这里的排版比较好看.

  添加了新的内容, 比如法线贴图和切空间的概念等(2019.07.04)

----------- 下面首先这是别人写的切空间的原理, 因为难懂所以我才写了一个新的版本的在后面 -----------

法线贴图中的法线向量在切线空间中,法线永远指着正z方向。切线空间是位于三角形表面之上的空间:法线相对于单个三角形的本地参考框架。它就像法线贴图向量的本地空间;
它们都被定义为指向正z方向,无论最终变换到什么方向。使用一个特定的矩阵我们就能将本地/切线空寂中的法线向量转成世界或视图坐标,使它们转向到最终的贴图表面的方向。 我们可以说,上个部分那个朝向正y的法线贴图错误的贴到了表面上。法线贴图被定义在切线空间中,所以一种解决问题的方式是计算出一种矩阵,把法线从切线空间变换到一个不同的空间,
这样它们就能和表面法线方向对齐了:法线向量都会指向正y方向。切线空间的一大好处是我们可以为任何类型的表面计算出一个这样的矩阵,由此我们可以把切线空间的z方向和表面的法线方向对齐。 这种矩阵叫做TBN矩阵这三个字母分别代表tangent、bitangent和normal向量。这是建构这个矩阵所需的向量。要建构这样一个把切线空间转变为不同空间的变异矩阵,
我们需要三个相互垂直的向量,它们沿一个表面的法线贴图对齐于:上、右、前;已知上向量是表面的法线向量。右和前向量是切线(Tagent)和副切线(Bitangent)向量。
下面的图片展示了一个表面的三个向量

计算出切线和副切线并不像法线向量那么容易。从图中可以看到法线贴图的切线和副切线与纹理坐标的两个方向对齐。我们就是用到这个特性计算每个表面的切线和副切线的。需要用到一些数学才能得到它们;请看下图:

上图中我们可以看到边纹理坐标的不同,是一个三角形的边,这个三角形的另外两条边是和,它们与切线向量和副切线向量方向相同。这样我们可以把边和用切线向量和副切线向量的线性组合表示出来(注意和都是单位长度,在平面中所有点的T,B坐标都在0到1之间,
因此可以进行这样的组合):

我们也可以写成这样:

是两个向量位置的差,和是纹理坐标的差。然后我们得到两个未知数(切线T和副切线B)和两个等式。你可能想起你的代数课了,这是让我们去接和。

上面的方程允许我们把它们写成另一种格式:矩阵乘法

尝试会意一下矩阵乘法,它们确实是同一种等式。把等式写成矩阵形式的好处是,解和会因此变得很容易。两边都乘以的逆矩阵等于:

这样我们就可以解出和了。这需要我们计算出delta纹理坐标矩阵的拟阵。我不打算讲解计算逆矩阵的细节,但大致是把它变化为,1除以矩阵的行列式,再乘以它的共轭矩阵。

有了最后这个等式,我们就可以用公式、三角形的两条边以及纹理坐标计算出切线向量和副切线。

我们可以用TBN矩阵把所有向量从切线空间转到世界空间,传给像素着色器,然后把采样得到的法线用TBN矩阵从切线空间变换到世界空间;法线就处于和其他光照变量一样的空间中了。 
我们用TBN的逆矩阵把所有世界空间的向量转换到切线空间,使用这个矩阵将除法线以外的所有相关光照变量转换到切线空间中;这样法线也能和其他光照变量处于同一空间之中。
我们来看看第一种情况。我们从法线贴图重采样得来的法线向量,是以切线空间表达的,尽管其他光照向量是以世界空间表达的。把TBN传给像素着色器,我们就能将采样得来的切线空间的法线乘以这个TBN矩阵,
将法线向量变换到和其他光照向量一样的参考空间中。这种方式随后所有光照计算都可以简单的理解。

以上就是别人写的攻略, 我表示有看没有懂, 就自己写一个吧

 

-------------------------- 我是分割线 --------------------------

好吧看完我要跪了, 有图有文, 可是看不懂, 我就来个深入浅出版本吧:

先说明什么是法线贴图 : 

  法线贴图就是提供给模型表面作为其法线的一张贴图, 或者叫凹凸贴图, 一般用来提高模型细节, 比如说一个墙壁模型师做成一个简单Quad, 然后加上凹凸贴图就能在光照的时候表现出墙面的凹凸效果了.

然后什么是切空间 : 

  法线贴图提取出来的向量, 它不是一个本地向量, 是基于该点所在的模型的三角面上的, 也就是基于切空间的, 那么问题来了, 这个法线它的方向是朝向哪里的? 也就是说这个法线的切空间坐标系是哪里来的呢? 我们看下图 : 

  图中可见, 如果要组成一个在这个面上的坐标系, 有无数种, 这些都是切空间坐标系.

  我们按照美术制作的流程 (假) 来解释比较容易明白, 首先美术做了一个面, 在这个面上要添加凹凸法线, 然后如果他选择了红色坐标系, 那么这个点的法线的向量可能是(1,2,3), 如果他选择了绿色的坐标系, 这个法线的向量可能就是(2,3,4)了,

他选择的坐标系就叫切空间了. 那么我们要怎样方便快捷地知道切空间呢? 是要美术在模型的每一个面上都附加一个坐标系信息吗? 还真是, 请看下图: 

  一个模型, 它的切空间信息是可以导入的, 也就是说模型是可以附带切空间信息的. 那么一个切空间信息应该是怎样的呢? 很简单, 因为模型的每个面都是有向量的 (当做Y轴), 那么我们再有一个其它的轴 (X轴或Z轴), 然后叉积就能计算出另外一个轴了.

所以导入模型的选项中可以选择导入Tangents, 这里的X轴一般被称为Tangent轴. Z轴被叫做Bitangent轴. Y轴就是Normal了.

  到这里你可能就慌了, 知道了切空间的各个轴, 也知道了在切空间中的凹凸法线向量, 那么怎样把凹凸法线变换到世界坐标系中啊??? 很简单, 先把凹凸法线转换到本地坐标系, 然后转换到世界坐标系.

比如:

  凹凸法线向量 (r, g, b) 一般使用r对应X轴, g对应Z轴, b对应Y轴, 所以法线贴图一般偏蓝, 就是偏向法线方向.

  切空间各个坐标轴向量(切空间相对于本地坐标系) : 

    X轴 : (x0, y0, z0) -> Tangent

    Y轴 : (x1, y1, z1) -> Normal

    Z轴 : (x2, y2, z2) -> Bitangent

  那么转换到本地坐标系就是 localNormal = normalize(Tangent * r + Normal * b + Bitangent * g), 数学理论不用说了, 初中生的知识. 再转到世界坐标系就用 Transform.localToWorldMatrix 计算即可, 非常简单. 看到这里就不虚了吧.

  我们继续往下看, 原来切空间还能通过计算得出来?

  为什么呢? 如果美术同学在导出模型的时候没有导出切空间信息给我们, 还能通过计算得到? 计算得到的跟美术同学制作时使用的切空间能一样吗?

  答案是 : 可以计算得到, 计算出来的切空间跟美术制作时使用的是一样的. 是不是又开始慌了? 不是说一个面上的切空间有无数种吗? 为什么能逆计算出来呢? 答案就在UV坐标中.

前面的文章是假设了T, B两个三维向量, 使用差值来计算的, 假设有三个点 : 

  P0 (x0, y0, z0) 对应UV(u0, v0)

  P1 (x1, y1, z1) 对应UV(u1, v1)

  P2 (x2, y2, z2) 对应UV(u2, v2)

  那么假设T,B向量为正交向量在三角平面上:

  P1-P0 = T * (u1-u0) + B * (v1-v0)

  P2-P0 = T * (u2-u0) + B * (v2-v0)

  根据上面文章的计算, 这个T,B向量是唯一的, 根据现代工程原理, 那么一般来说美术制作所使用的软件, 它也是根据模型的顶点位置和UV来给出切空间的, 然后美术同学就在给出的切空间去做凹凸贴图, 而不是由他来自定义切空间.

所以切空间是可以根据逆计算得到的.

 

下面是从几何原理来说明切空间: 

先从shader怎样使用凹凸贴图开始说, 原理很简单, 首先你想要给一个模型提供法线贴图, 那么在每一个Fragment阶段都要去取​NormalMap的rgb当做法线来用, 流程如下:

    1. 用uv取出NormalMap相应的rgb作为tangentNormal, 它的rgb的b值是我们通常的法线方向. 见图一
    2. 把这个tangentNormal贴到uv相应的插值点的Local坐标位置(图二), 因为它表现的是这个点的切空间中的法线方向, 必然要转换到本地坐标系, 转换​之后它就是这个点的LocalNormal了.

   如图一是tangentNormal的rgb(xyz)方向. 图二表示这个图元在模型的一个面上, tangentNormal​在转换后的方向也​发生了改变.        
    3. 把LocalNormal转到世界就是该插值点的世界法线了WorldNormal. 完毕.

图一

 

图二

 

通过代码梳理流程, 以下是某老外写的, 思路非常清晰 :
  1. GetTangentSpaceNormal就是把法线贴图的向量弄出来
  ​2. 获取出来的tangentSpaceNormal就是一个向量, 它还不能称为法线,    注意这里使用了rgb的b来作为法线方向的值.
  3. i.tangent (X轴), binormal (Z轴), i.normal (Y轴) 代表的就是当前三角面的空间相对于, LocalSpace的坐标系, 其实就是新坐标系的x,z,y轴(想象想象), 这样跟tangentSpaceNormal的每个值相乘, 就相当于把向量投影到切空间里了,

   最终值就是该点的本地坐标系的最终法线方向.

 

// 把法线贴图的向量弄出来
float3 GetTangentSpaceNormal (Interpolators i) { float3 normal
= float3(0, 0, 1); #if defined(_NORMAL_MAP) normal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale); #endif #if defined(_DETAIL_NORMAL_MAP) float3 detailNormal = UnpackScaleNormal( tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale ); detailNormal = lerp(float3(0, 0, 1), detailNormal, GetDetailMask(i)); normal = BlendNormals(normal, detailNormal); #endif return normal; }

 

void InitializeFragmentNormal(inout Interpolators i) {

    float3 tangentSpaceNormal = GetTangentSpaceNormal(i);

    #if defined(BINORMAL_PER_FRAGMENT)

        float3 binormal = CreateBinormal(i.normal, i.tangent.xyz, i.tangent.w);

    #else

        float3 binormal = i.binormal;

    #endif

    

    i.normal = normalize(

        tangentSpaceNormal.x * i.tangent +
​        tangentSpaceNormal.z * i.normal + 
        tangentSpaceNormal.y * binormal +       

    );

}

  这里可能有点没有说清楚, i.tangent, binormal, i.normal其实都是三角形面上基于LocalSpace坐标系的新坐标系(切空间), 而它的法线就是i.normal.

 因为NormalMap的b(z)表示的是垂直方向, 所以用tangentSpaceNormal.z * i.normal 来获得在新坐标系中法线方向的值. 

FragmentOutput MyFragmentProgram (Interpolators i) {

    float alpha = GetAlpha(i);

    #if defined(_RENDERING_CUTOUT)

        clip(alpha - _AlphaCutoff);

    #endif


    InitializeFragmentNormal(i);

 

  看, 在Fragment中修改法线方向

  在前面的流程梳理中很自然地忽略了一个过程: 怎样获得Tangent和Bitangent轴.实际上就是获得一个在三角形面上的坐标系, 我们将LocalSpace坐标系作为原始坐标系, 而在模型三角形面上的坐标系(切空间)就是LocalSpace坐标系的子坐标系, 
它的每个轴的描述是用LocalSpace坐标系作为参照的.所以Tangent和Bitangent的计算可以直接在模型阶段就预先计算好, 作为本地数据存储即可.

 

  Unity的模型导入就有Tangent计算/导入选项.


  那么Tangent和Bitangent轴到底是怎样计算出来的呢, 以现有数据来看, 我们只知道三角形面的几个顶点坐标, 以及该面的Normal(法线), 那么在这个三角形上构建的坐标系可以是无穷多的, 只要符合在面上的两个正交向量+法线即可,

看下图 :

  法线(红)+蓝色 或 法线(红)+绿色 都能构建一个坐标系. 法线贴图获取的向量在不同坐标系里面的方向肯定是不同的. 要怎样才能构建唯一正确的切空间坐标系呢...回到法线贴图来,

当把这个贴图贴在某个模型上时, 比如在下图中, 喷涂区域贴在了某个三角面上 : 

 

  喷涂区域就是对应的三角面, 那么就简单了, 如果我们把这张2D图片做成一个3D中的平面的话, 我们通过拉伸, 平移旋转等各种方法把对应的三角形区域跟模型上的重叠起来的话,那么该3D平面的两个边就成了Tangent和Bitangent轴了,
理解了的话就可以 回去看开篇的数学公式了 往下看了. 下图我把中国地图贴在了一个三角形上(假设是在模型的本地坐标系中), 然后做了一个在这个坐标系中的3D平面挂上贴图.

  我通过各种方法使他们图片重叠了, 这样我的3D图片的两个边 ( 当然是UV的正方向)就成为了切空间的Tangent和Bitangent了(当然计算切空间不可能这样神手动, 请往下看 ).

  希望这个能够讲清楚切空间的逻辑流程...
  PS: 模型每个顶点都带有position, uv, 所以计算Tangent这些数据并不依赖于图片, 不要被上面我的手动误导了哈

  下来详细讲解数学流程吧...还是用中国地图来说: 

 

  在上面的步骤中我们把地图的板子跟模型的对应三角形重叠了, 通过手动方式获取了Tangent向量 ( 注意由于有叉乘的存在, 用Vector3.Cross(Normal, Tangent)就可以获得Bitangent了, 所以不需要浪费空间去存储Bitangent,基本很多引擎都不保存Bitangent),
那么如何通过数学方式快速正确地获取Tangent呢, 上图中有几个变量:
​    T, B 就是Tangent和Bitangent 是我们要求出来的.
​    P1,P2,P3 就是模型三角形面的三个点了, 他们带有位置和UV信息.
​    P1{X1, Y1, Z1, U1, V1}
​    P2{X2, Y2, Z2, U2, V2}
​    P3{X3, Y3, Z3, U3, V3}

 

    E1, E2是我们临时计算用到的信息, 就是两点组成的向量
​    E1{P1 - P2} (X, Y, Z)
​    E2{P3 - P2} (X, Y, Z)
​    注意, 这是计算用到的中间变量, 与取哪个点的先后无关, 与哪个点的相对位置也无关, 不管怎样取只要能表现出三角形的任意两条边即可.
​    du1, du2, dv1, dv2 分别表示E1, E2代表的向量在uv上的差值
​    注意, 这里因为要求得的向量只有T,B所以需要两个行列式即可, 所以上面的数据只取了三角形的任意两条边, 以及他们的增量数据du/dv.
​​
​变量就这些, 它已经提供了我们所需的数据了
​  1. 它有了实际空间中的两个向量E1, E2
​  2. 它提供了向量增长的方向的参考数据du1, dv1, du2, dv2, 也就是说E1,E2在T,B坐标系下是如何增长的(因为UV就是沿着T,B增长的), 反过来也就可以求出T,B的向量了.
​  PS -- 这里可以把T,B坐标系看成是有边界的坐标系(UV值就是坐标系中的位置所占的百分比), 之后的计算能够进行全依赖于UV坐标是个归一化数据, 在任何缩放下都不受影响的功劳.

​之后就可以开始写等式了​: 

 

  与上图中的几何信息完全相符, 而T, B也写成向量形式, 因为它被映射到了实际空间里经过了缩放(参考我手动Tangent的图), 计算出来的方向是正确的, 最后会取它的归一化向量.
T, B都是基于LocalSpace空间下的子坐标系, 所以可以用一般向量来表示T, B的轴向 ( 这里就用上文的转换公式了 ) : 
 
等式转为行列式 : 
 
求T,B向量 : 
然后得到 : 
 
  到这里就求出了T(Tx, Ty, Tz) 与B(Bx, By, Bz)的坐标轴了, 而NormalMap的向量与Tangent, Bitangent, Normal都一样属于LocalSpace坐标系, 那么NormalMap向量在切空间的方向就是在切空间各个轴上的投影了...
 
 之前的文章写得有点乱, 开始整理一下, 感觉脑子被驴踢了(2019.07.04)
 
 
 

 

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!