深刻浅出之切空间

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

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

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

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

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

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

咱们也能够写成这样:

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

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

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

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

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

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

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

 

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

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

先说明什么是法线贴图 : ip

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

而后什么是切空间 : it

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

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

  咱们按照美术制做的流程 (假) 来解释比较容易明白, 首先美术作了一个面, 在这个面上要添加凹凸法线, 而后若是他选择了红色坐标系, 那么这个点的法线的向量多是(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)
相关文章
相关标签/搜索