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);

### Unity Shader 中不同空间的概念及其用途 #### 1. **世界空间 (World Space)** 世界空间是全局的空间坐标系,所有的对象都位于这个世界空间中。每个物体都有自己的本地空间(Local/Object Space),当这些物体被放置到场景中时,它们会被转换世界空间。 - 物体的顶点位置、法线和其他属性通常是在其自身的局部空间中定义的。 - 使用 `mul(UNITY_MATRIX_M, localPosition)` 或者内置函数 `UnityObjectToWorld` 可以将顶点从物体空间转换世界空间[^2]。 - 世界空间对于处理光照计算非常重要,因为光源的位置通常是基于世界坐标的。 ```c++ fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal)); ``` --- #### 2. **视图空间 (View/Camera Space)** 视图空间是以摄像机为原点的空间坐标系,在该空间中,所有的东西都被描述为相对于当前活动摄像机的位置和朝向。 - 当我们希望执行某些依赖于观察者的操作(如深度测试或雾效)时,视图空间非常有用。 - 转换到视图空间可以通过乘以模型视图矩阵完成:`mul(UNITY_MATRIX_V, worldPosition)`[^4]。 --- #### 3. **裁剪空间 (Clip Space)** 裁剪空间是对经过投影变换后的顶点进行进一步裁剪的一个中间阶段。在这个空间里,超出视野范围的部分会被丢弃掉。 - 统一的裁剪平面方程决定了哪些部分应该保留下来或者剔除出去。 - 在顶点着色器中,最终输出的顶点位置必须处于裁剪空间下,这一步骤由 `gl_Position` 完成[^1]。 ```c++ o.vertex = UnityObjectToClipPos(v.vertex); ``` --- #### 4. **屏幕空间 (Screen Space)** 屏幕空间是指已经映射到了实际显示设备分辨率上的二维坐标系统。此时,三维几何已经被扁平化成为适合呈现的形式。 - 这里的单位一般对应像素数;因此,任何后期特效比如模糊、边缘检测等都会在此级别上实施。 - OpenGL 和 DirectX 对齐方式略有差异需要注意调整偏移量等问题。 --- #### 5. **切线空间 (Tangent Space)** 切线空间是一种局部化的坐标框架,主要用于正常贴图的应用场合。它围绕每个单独的三角网格构建了一个新的基底集合——即所谓的 TBN 坐标轴组(T代表切线 Tangent,B代表双切线 Binormal/BiTangent,N则指代标准意义上的 Normal 法线矢量)。这种设置允许更精确地控制表面细节表现而无需增加额外几何复杂度。 - 正常贴图中的数据存储的是相对此特殊参照体系下的偏差信息而非绝对方向指示。 - 构建完整的 TBN 矩阵以便能正确解读来自纹理文件内的数值变化趋势至关重要[^4]。 ```c++ float3x3 tangentToLocal; tangentToLocal[0] = normalize(i.tangent.xyz); tangentToLocal[1] = cross(i.normal, i.tangent.xyz) * i.tangent.w; tangentToLocal[2] = i.normal; half3 normalInTangentSpace = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); half3 viewDirInTangentSpace = mul(tangentToLocal, viewDir); ``` --- ### 总结 以上介绍了几种常见的空间类型以及各自的作用领域。理解这些不同的空间如何相互作用有助于开发者编写更加高效灵活的 Shaders 。每种空间的选择取决于具体应用场景需求,合理运用可极大提升渲染效率与画质水平。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值