You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

手动计算切线异常求助:法线映射切线方向错误排查

手动计算OBJ模型切线实现法线映射时切线方向突变问题

我正在加载OBJ文件并手动计算切线以实现法线映射,参考LearnOpenGL的内容。目前切线显示异常,经TBN矩阵转换后的世界空间法线也错误,截图中可见多处(尤其是相邻共面三角面区域)切线方向突变。

切线计算代码

for (uint32_t i = 3; i < newMesh.indices.size(); i += 3) {
    uint32_t vertex0Index = newMesh.indices[i - 3];
    uint32_t vertex1Index = newMesh.indices[i - 2];
    uint32_t vertex2Index = newMesh.indices[i - 1];

    auto& v0 = newMesh.vertices[vertex0Index];
    auto& v1 = newMesh.vertices[vertex1Index];
    auto& v2 = newMesh.vertices[vertex2Index];

    glm::vec3 tangent(0.0f);
    glm::vec3 edge1 = v1.position - v0.position;
    glm::vec3 edge2 = v2.position - v0.position;
    glm::vec2 deltaUV1 = v1.uv - v0.uv;
    glm::vec2 deltaUV2 = v2.uv - v0.uv;

    float f;
    float denominator = deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y;
    if (std::fabs(denominator) <= std::numeric_limits<float>::epsilon()) {
        tangent = v1.position - v2.position;
    }
    else {
        f = 1.0f / denominator;

        tangent.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
        tangent.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
        tangent.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
    }

    assert(!isnan(tangent.z));

    // Accumulate tangent
    v0.tangent += tangent;
    v1.tangent += tangent;
    v2.tangent += tangent;
}

我已处理分母为0的情况避免除零错误,但无明显改善。

切线可视化着色器代码

顶点着色器

layout (location = 4) out vec3 tangent;

...
// Build TBN matrix
vec3 T = normalize(normalMatrix * vTangent);
vec3 N = normalize(normalMatrix * vNormal);
// Make sure the vectors are still orthogonal
T = normalize(T - N * dot(T, N));
vec3 B = normalize(cross(N, T));
// Handedness
if (dot(cross(N, T), B) < 0.0f) {
    T = T * -1.0f;
}
...
tangent = T;

片段着色器

layout (location = 4) in vec3 tangent;
...
outFragColor = vec4(tangent, 1.0f);

问题排查方向与改进建议

  • 检查顶点UV的唯一性:OBJ文件中常出现同一空间位置对应多个UV坐标的情况,若未将顶点按「位置+UV」组合拆分,多个三角面的切线会累加在同一个顶点上,导致方向冲突。确保每个唯一的(位置, UV)对对应独立顶点。
  • 修正切线计算公式:当前切线计算的UV分量使用有误,标准公式应为:
    f = 1.0f / denominator;
    tangent.x = f * (deltaUV2.x * edge1.x - deltaUV1.x * edge2.x);
    tangent.y = f * (deltaUV2.x * edge1.y - deltaUV1.x * edge2.y);
    tangent.z = f * (deltaUV2.x * edge1.z - deltaUV1.x * edge2.z);
    
    错误的UV分量匹配会直接导致切线方向偏离预期。
  • 添加累加后的切线归一化:当前仅对每个三角面的切线进行累加,未对顶点最终的切线向量做归一化处理。在所有三角面遍历完成后,执行v.tangent = glm::normalize(v.tangent),确保向量方向统一。
  • 优化分母为0时的 fallback 逻辑:当UV共线时,用v1.position - v2.position作为切线无法保证与法线垂直,会引发后续正交化的方向突变。正确做法是生成与法线垂直的向量:
    glm::vec3 tmpTangent = glm::cross(glm::vec3(0.0f, 1.0f, 0.0f), v0.normal);
    if (glm::length(tmpTangent) < 1e-6) {
        tmpTangent = glm::cross(glm::vec3(1.0f, 0.0f, 0.0f), v0.normal);
    }
    tangent = glm::normalize(tmpTangent);
    
  • 调整TBN矩阵的 handedness 处理:当前着色器中翻转T的逻辑会破坏正交性,建议在CPU计算切线时存储 handedness 符号(比如存入tangent的w分量),在着色器中用该符号调整副切线B:
    // CPU端计算时
    float handedness = denominator < 0.0f ? -1.0f : 1.0f;
    v0.tangent.w = handedness;
    // 着色器中
    vec3 B = normalize(cross(N, T)) * vTangent.w;
    
  • 验证法线矩阵的正确性:确保normalMatrix是模型视图矩阵的逆转置,若模型存在非均匀缩放,直接使用模型矩阵的旋转部分会导致法线、切线变形。

内容的提问来源于stack exchange,提问作者bishopp

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 01:27:10