从建模软件的计算、到Unity的导入,再到最终的Shader构建,切线空间的计算是一套贯穿整个美术-技术流程的完整逻辑。不过这里需要先澄清一个关键点:切线空间的核心数据(切线 Tangent、手性标志 w),是在导入Unity时就已经计算好的,而不是在Shader运行时动态生成的。Shader的任务只是使用这些数据。
下面,我们就按数据从生成到应用的完整生命周期来梳理。
✍️ 第一步:在建模软件中“种下种子”
在3D建模软件里,艺术家们虽然很少手动计算切线,但他们的工作——UV展开,是整个计算过程的基石。
核心原理:切线空间的定义(X轴为切线T,Y轴为副法线B)与模型的UV 坐标系直接挂钩。切线T的方向被设计为与UV的U方向在模型表面上对齐。
数学推导:给定一个三角形,其三个顶点的位置和UV坐标,存在一个3x3的线性变换,能将UV坐标的增量(ΔU, ΔV)映射到世界空间的位置增量上。解这个方程组,就能得到切线T和副法线B的初始方向。
实际问题:镜像UV的陷阱:为了最大化利用贴图空间,模型会镜像另一半的UV,但这会破坏UV的连续性,导致镜像部分的T/B方向无法与法线贴图正确匹配,产生光影错误。
🤖 第二步:Unity导入时的“智能修复” (MikkTSpace)
为了解决镜像UV导致的切向量方向错乱问题,Unity在模型导入阶段使用业界标准算法——MikkTSpace来完成计算。它的核心目标,就是为每个顶点计算出能保证法线贴图在所有情况下(包括镜像)都正确显示的切线数据。
MikkTSpace 工作流程:
分析网格与UV:读取模型的顶点位置、法线(Normal)和UV信息。
计算和平均化:为每个三角形面计算切线T和副法线B,并对共享顶点的面进行平均化处理。
正交化处理:强制调整切线T,使其与顶点法线N垂直,确保TBN坐标系的精确性和稳定性。
计算关键信号——
tangent.w:通过检查T/B/N向量构成的手性(右手/左手系)计算出tangent.w。w的值是+1或-1,代表了UV是否发生了镜像。存储结果:将最终的正交化切线T(xyz分量)和手性标志w存储在
Vector4类型的tangent变量中。
正如官方文档,此算法已成为业界标准,被广泛用于各大3D建模软件包、法线贴图工具和图形引擎中。
🎮 第三步:在Shader中“激活”数据
当模型进入游戏后,Shader扮演“执行者”的角色。标准写法如下:
// 从模型数据中获取法线(normalOS)和切线(tangentOS) v2f vert (appdata v) { // ... 将法线和切线从模型空间转换到世界空间 float3 normalWS = TransformObjectToWorldNormal(v.normalOS); float4 tangentOS = v.tangent; // Unity提供的切线是float4类型 float3 tangentWS = TransformObjectToWorldDir(tangentOS.xyz); // 根据tangent.w构建正确的副法线(Bitangent/Binormal) float3 bitangentWS = cross(normalWS, tangentWS) * tangentOS.w; // 构建并传递TBN矩阵 o.tspace0 = half3(tangentWS.x, bitangentWS.x, normalWS.x); o.tspace1 = half3(tangentWS.y, bitangentWS.y, normalWS.y); o.tspace2 = half3(tangentWS.z, bitangentWS.z, normalWS.z); // ... }需要注意的是,Unity旧版内置管线和基于
UnityCG.cginc的代码,可能使用TANGENT_SPACE_ROTATION宏来构建TBN矩阵,但其核心原理是完全一致的。
🏗️ 重建TBN矩阵与转换法线
在片元着色器中,需要重建TBN矩阵,用法线贴图的数据替换原本的法线。
// 从法线贴图采样并解码,得到切线空间下的法线数据(tangentNormal) float3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv)); // 构建TBN矩阵,将切线空间法线转换到世界空间 float3x3 TBN = float3x3(i.tspace0, i.tspace1, i.tspace2); float3 worldNormal = normalize(mul(tangentNormal, TBN));💡 核心提示:理解tangent.w
理解tangent.w的作用对于正确实现法线贴图至关重要,它能确保在镜像区域的光照方向始终正确。这个流程在Unity中通常是全自动的。但当我们需要手动构建TBN矩阵或在运行时动态调整模型时,对这些细节的理解就变得至关重要了。