从建模软件的计算、到Unity的导入再到最终的Shader构建切线空间的计算是一套贯穿整个美术-技术流程的完整逻辑。不过这里需要先澄清一个关键点切线空间的核心数据切线 Tangent、手性标志 w是在导入Unity时就已经计算好的而不是在Shader运行时动态生成的。Shader的任务只是使用这些数据。下面我们就按数据从生成到应用的完整生命周期来梳理。✍️ 第一步在建模软件中“种下种子”在3D建模软件里艺术家们虽然很少手动计算切线但他们的工作——UV展开是整个计算过程的基石。核心原理切线空间的定义X轴为切线TY轴为副法线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是否发生了镜像。存储结果将最终的正交化切线Txyz分量和手性标志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矩阵或在运行时动态调整模型时对这些细节的理解就变得至关重要了。