Unity切线空间转换

        切线空间是一个非常重要的空间。这体现在两个点,首先它的xy方向是UV方向,所以将纹理坐标和空间坐标建立了关联。其次它的z方向是法线,这很方便基于切线空间定义法线贴图。

        所以它的用途一个是用来处理法线贴图,这非常常用。另外就是可以利用它和贴图UV对应,比如视差贴图可以用来确纹理坐标定偏移。

        它是Unity坐标系中唯二使用右手坐标系的空间,另外一个是观察空间。这很容易看出来,Unity中纹理坐标是从左下角开始的,往右侧是U方向,往上是V方向,按照右手定则Z轴是正方向。

        本文主要分析切线空间与其他空间的转换。

切线空间到世界空间

        首先分析最常用的切线空间变换。我们知道,坐标系A到坐标系B的转换矩阵,可以用A坐标系三个坐标轴的基向量在B坐标系的坐标作为列向量构成。很显然,要得到切线空间到世界空间矩阵,需要切线空间三个坐标轴的世界空间坐标。这很容易办到,只需要将切线、副切线、法线分别转换到世界空间就可以。

        首先需要采样法线贴图,计算切线空间的法线方向,然后转到世界空间,代码如下:

float3 tagentNormal = UnpackNormal(tex2D(_NormalTex, i.uv.xy));
tagentNormal *= _NormalStrength;
tagentNormal.z = sqrt(1.0 - saturate(dot(tagentNormal.xy, tagentNormal.xy)));
float3x3 tangent2World = float3x3(i.worldTangent, i.worldBitangent, i.worldNormal);
float3 worldNormal = mul(tagentNormal, tangent2World);

        有一点需要注意,float3x3的构造是行优先的,也就是说构造的矩阵第一行是切线,第二行是副切线,第三行是法线。这跟我们需要的列矩阵是不符合的。我们必须对这个矩阵进行转置,而CG/HLSL中转置最方便的方法就是调换向量和矩阵的顺序,这也是上面使用了向量右乘矩阵的原因。

法线的其他求法

        在不使用切线空间法线贴图的情况下,我们也可以通过其他方法求出法线。根据《Unity Shader入门精要》的推导,如果模型矩阵M是正交矩阵,那么可以直接用M矩阵变换法线不产生误差。如果模型矩阵包含系数为k的统一缩放,那么可以用1/k*M来得到法线变换矩阵。Unity中有一个单独的编译选项,用来指定只包含统一缩放这种情况:

#pragma instancing_options assumeuniformscaling

         如果在shader中加上这个编译选项,那么就会开启UNITY_ASSUME_UNIFORM_SCALING宏,只有不满足这个宏条件才需要使用模型矩阵的逆转置矩阵计算,比如这是Unity内部TransformObjectToWorldNormal函数的实现:

// Transforms normal from object to world space
float3 TransformObjectToWorldNormal(float3 normalOS, bool doNormalize = true)
{
#ifdef UNITY_ASSUME_UNIFORM_SCALING
    return TransformObjectToWorldDir(normalOS, doNormalize);
#else
    // Normal need to be multiply by inverse transpose
    float3 normalWS = mul(normalOS, (float3x3)GetWorldToObjectMatrix());
    if (doNormalize)
        return SafeNormalize(normalWS);

    return normalWS;
#endif
}

        模型矩阵的逆转置矩阵就是世界空间到物体空间的变换矩阵的转置,这也是上面得到WorldToObjectMatrix后需要使用向量右乘矩阵的原因。

切线空间到物体空间

        有了上面的铺垫,这个就很简单了,需要切线空间三个坐标轴的基向量在物体空间的坐标,这实际上就是顶点数据中存储的切线和法线,副法线按照右手法则,需要是Z轴叉乘X轴,也就是cross(normal,tangent),示例代码如下:

float3x3 tangentToObject = float3x3(
	v.tangent.xyz,
	cross(v.normal, v.tangent.xyz) * v.tangent.w,
	v.normal
);
i.objectViewDir = mul(tangenetSpaceViewDir(v.vertex), tangentToObject);

物体空间到切线空间

        和上面是相似的,只是需要求个逆。但是注意切线空间相对物体空间只发生了旋转和位移,不存在缩放(和相机相对世界空间的变换类似)。因为变换的是方向不需要考虑位移,所以变换矩阵肯定是正交矩阵,正交矩阵的逆等于它的转置,所以我们实际上只需要转置即可。这意味着要将向量右乘矩阵变成左乘。

float3x3 tangentToObject = float3x3(
	v.tangent.xyz,
	cross(v.normal, v.tangent.xyz) * v.tangent.w,
	v.normal
);
i.tangentViewDir = mul(tangentToObject, ObjSpaceViewDir(v.vertex));

         这个变换也很常用,比如在视差贴图中,需要用这个矩阵计算相机方向上纹理坐标空间的偏移。

世界空间到切线空间

        功能自然是将世界空间坐标映射到纹理坐标,需要世界空间三个坐标轴的基向量在切线空间的坐标,这三个坐标一般不好给出,有两种思路:

        1. 求出切线空间到世界空间的逆矩阵。当假定统一缩放时,可以直接求转置;否则需要求逆,然而这在Shader中并不现实

        2. 分成两步,首先从世界空间变换到物体空间,然后将物体空间变换到切线空间。这比较好办,代码如下:

ObjSpaceViewDir = TransformWorldToObjectDir(WorldSpaceViewDir)
float3x3 tangentToObject = float3x3(
	v.tangent.xyz,
	cross(v.normal, v.tangent.xyz) * v.tangent.w,
	v.normal
);
i.tangentViewDir = mul(tangentToObject, ObjSpaceViewDir);

猜你喜欢

转载自blog.csdn.net/paserity/article/details/130215315