谈谈 TANGENTA_SPACE_ROTATION

我们知道 TANGENTA_SPACE_ROTATION 是内建在 UnityCG.cginc 中的一个宏,作用是实现从模型空间到切线空间的变换。那么究竟具体是怎么变换的了?

首先,什么是切线空间?

切线空间,简单来说就是一个坐标系。对于模型的每个顶点,都有一个切线空间,坐标系的原点就是顶点,切线方向(t)即使是 X 轴,顶点的法线方向(n)就是 Z 轴,Y 轴就是副切线方向(法线和切线叉积可得到,有两个方向,可根据 v.tangent.w 决定选取哪一条作为副切线)。

tangent_space_coord.jpeg

图片来自:《Unity Shader 入门精要》

在使用法线纹理时我们一般都会使用到切线空间。当使用的是切线空间下的法线纹理时,由于每个法线位于各自的切线空间下(每个切线空间都不一样,下面会讲到什么是切线空间而导致不一样),这种法线纹理存储的就是每个点在各自切线空间中的法线扰动方向。

接着,我们来看看 TANGENTA_SPACE_ROTATION 具体的代码,如下:

1
2
3
4
5
// Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION \
float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
}
  • 代码首先使用模型空间下的法线方向和切线方向叉积得到了副切线方向 binormal(叉积结果与 v.tangent.w 进行相乘,因为与法线和切线都垂直的方向有两个,与 v.tangent.w 相乘决定选择其中哪一个方向)。

  • 然后定义了 3x3 变换矩阵 rotation,分别将切线方向、副切线方向和法线方向按行摆放组成了这个矩阵。

为什么这样得到的矩阵就是从模型空间到切线空间的变换矩阵了?

我们知道,要得到一个从 C 空间到 P 空间的变换矩阵,只需要将 C 空间在 P 空间下表示的坐标轴矢量以及原点按列摆放构建得到的就是 C -> P 的变换矩阵(坐标空间变换基础)。

因此我们想要求得模型空间到切线空间的变换矩阵时,只需要将模型空间在切线空间下表示的坐标轴和原点按照列摆放即可。求模型空间到切线空间的变换矩阵比较麻烦,但是得到切线空间到模型空间的变换矩阵却很容易,切线空间到模型空间的变换矩阵的逆矩阵就是我们需要的矩阵。切线空间到模型空间的变换矩阵就是切线坐标轴和原点在模型空间下按列摆放得到的矩阵,即只需要将切线方向(X轴)、副切线方向(Y轴)和法线方向(Z轴)按列摆放即可。我们知道,如果一个变换中仅存在旋转和平移变换(正交矩阵),那么这个变换矩阵的逆矩阵就等于它的转置矩阵,从切线空间到模型空间的变换正好满足这种情况,因此我们就得到了模型空间到切线空间的变换矩阵:切线坐标轴和原点在模型空间下按行摆放得到的矩阵,即将切线方向(X轴)、副切线方向(Y轴)和法线方向(Z轴)按行摆放得到的矩阵。

需要注意的是,在 TANGENTA_SPACE_ROTATION 得到的 rotation 是一个 3x3 的矩阵,也就是说这个矩阵是用来对方向矢量进行坐标空间变换的。因为矢量是没有位置的,因此坐标空间的原点可以忽略。

这就是 TANGENTA_SPACE_ROTATION 得到的 rotation 矩阵为什么是从模型空间到切线空间的变换矩阵的原理。

再来类推一下世界空间到切线空间的变换矩阵

首先,我们计算出切线空间各个坐标轴在世界空间下的表示:

  • 世界空间下顶点处的切线方向(X 轴) fixed3 worldTangent = UnityObjectToWorldDir(v.tangent);

  • 世界空间下顶点处的法线方向(Z 轴) fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);

  • 世界空间下顶点处的副切线方向(Y 轴) fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

然后构建切线空间到世界空间的变换矩阵,将上面三个计算得到的值按照 X、Y、Z 轴分别按照列摆放得到切线空间到世界空间的变换矩阵。如下:

1
2
3
4
5
6
float4x4 tangentToWorldMatrix = float4x4(
half4(worldTangent.x, worldBinormal.x, worldNormal.x, 0.0),
half4(worldTangent.y, worldBinormal.y, worldNormal.y, 0.0),
half4(worldTangent.z, worldBinormal.z, worldNormal.z, 0.0),
half4(0.0, 0.0, 0.0, 1.0)
);

世界空间到切线空间的变换矩阵就是上面矩阵的转置矩阵(将切线、副切线和法线按行摆放),如下:

1
2
3
float3x3 worldToTangentMatrix = float3x3(
worldTangent, worldBinormal, worldNormal.z
);

参考

  • 《Unity Shader 入门精要》