直接用顶点法线和世界坐标轴计算切线会出错,因为切线空间必须与UV梯度对齐,而非全局方向;正确方法是基于三角面内顶点位置与UV偏移解算切线T和副切线B,并按顶点累加归一化,同时通过叉积点积校正bitangent手性,最后以vec4(T, tangentW)输出供shader使用。

为什么直接用顶点法线和世界坐标轴算 tanget 会出错
切线空间不是由全局方向决定的,而是紧贴网格表面局部变化的方向。如果用 normalize(cross(worldUp, normal)) 这类方法硬凑,结果在 UV 拉伸、三角面倾斜或模型镜像翻转时完全失准——bitangent 方向尤其容易反向,导致法线贴图闪烁或光照翻转。
真正可靠的切线必须与顶点的 UV 坐标梯度对齐:UV 变化越快的方向,对应切线方向越强。这意味着必须用相邻顶点的几何偏移(deltaPos)和 UV 偏移(deltaUV)联合求解。
实操建议:
- 每个三角形面内,取三个顶点
v0,v1,v2及其对应的uv0,uv1,uv2 - 计算边向量:
edge1 = v1 - v0,edge2 = v2 - v0;UV 差:deltaUV1 = uv1 - uv0,deltaUV2 = uv2 - uv0 - 用二元一次方程组解出切线
T和副切线B(即 bitangent):f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y)T = f * (deltaUV2.y * edge1 - deltaUV1.y * edge2)B = f * (-deltaUV2.x * edge1 + deltaUV1.x * edge2) - 对每个顶点,把所属所有三角面算出的
T和B累加后归一化(注意:要按面面积加权更稳,但多数场景简单平均已够用)
如何处理 UV 反向和奇异面导致的 bitangent 符号错误
当某三角面的 UV 映射发生镜像(比如 Maya 中手动翻转 UV 岛),行列式 deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y 为负,此时解出的 T 和 B 仍数学正确,但最终 handedness(手性)可能与法线贴图期望不一致——典型表现是部分区域法线贴图左右颠倒。
立即学习“C++免费学习笔记(深入)”;
标准做法是在生成切线后,统一检查并修正手性:
- 对每个顶点,先归一化
T和N(法线),再算B = normalize(cross(N, T)) - 计算原始解出的
B与这个叉积结果的点积:dot(original_B, cross(N, T)) - 若点积为负,说明需要翻转
B:B = -B,并记录该顶点的tangent.w = -1.0f(D3D/GL 通用约定,用于 shader 中动态重建正确bitangent) - 最终顶点输出的
tangent向量应为vec4(T.x, T.y, T.z, tangentW),其中tangentW是 ±1
C++ 实现中容易漏掉的内存与索引陷阱
网格数据常以索引缓冲(std::vector<uint32_t></uint32_t>)+ 顶点缓冲(std::vector<vertex></vertex>)方式组织,但切线计算必须基于「面拓扑」而非「顶点索引流」——即不能直接对 indices[i], indices[i+1], indices[i+2] 拿顶点算,因为共享顶点的 UV 或法线可能不同(如接缝处)。你实际需要的是「面角顶点」粒度的数据。
推荐结构:
- 先展开所有三角形角:遍历每个 face,把
(v0, v1, v2)和对应(uv0, uv1, uv2)存入临时数组(长度 =indices.size()) - 为每个角单独计算
T和B,再按顶点 ID 累加(用std::vector<:vector>></:vector>存各顶点的所有角切线) - 避免用
std::map或哈希做顶点去重——实时计算中它比朴素 vector 查找还慢;预处理阶段可用std::unordered_map,但记得 reserve 容量 - 若顶点结构含
padding(如float tangent[4]对齐到 16 字节),确保sizeof(Vertex)正确,否则 GPU 读取会错位
OpenGL / Vulkan 下传入 shader 前的最后校验点
即使 C++ 端算得再准,shader 里用错也会白忙:GLSL 中 mat3 TBN 必须由 normalize(T), normalize(B), normalize(N) 组成列向量(不是行),且 TBN 应用于将法线贴图采样值从切线空间转到世界/视图空间。
关键确认项:
- vertex shader 中是否做了
T = normalize(mat3(model) * T);?未变换的切线在 world space 下无法与 world-space normal 正交 - 是否用了
flat修饰符误传了切线?切线必须插值(smooth),否则三角面内突变 - Vulkan 中
VkVertexInputAttributeDescription的format是否设为VK_FORMAT_R32G32B32A32_SFLOAT?tangent.w若被截断为VEC3,手性信息就丢了 - 调试时可临时输出
T.x到屏幕:纯红区域表示切线 X 分量全为正,若出现块状色斑,说明某处累加未归一或符号未统一
最常被跳过的环节是:没验证 UV 是否真的随几何连续——比如一个圆柱体用自动展开 UV,顶部一圈顶点 UV.y 接近 0 和 1,差值接近 1,会导致 deltaUV 计算溢出。这种 case 必须在预处理中检测并修复 UV 边界跳变。


















