深入浅出之切空间

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

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

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

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

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

我们也可以写成这样:

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

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

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

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

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

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

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

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

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

概念: 首先你想要给一个模型提供法线贴图, 那么在每一个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就是一个向量, 它还不能称为法线,
    注意这里使用了b(z)来作为法线方向的值.
3. i.tangent, binormal, i.normal 代表的就是当前三角面的空间相对于
    LocalSpace的坐标系, 其实就是新坐标系的x,z,y轴(想象想象), 这样乘过去
    就相当于把向量转到(投影到)切空间里了. 最终值就是该点的LocalNormal.

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向量在切空间的方向就是在切空间各个轴上的投影了...
 
 
 
 
 

猜你喜欢

转载自www.cnblogs.com/tiancaiwrk/p/11132437.html