计算 3D 切线空间

Posted

技术标签:

【中文标题】计算 3D 切线空间【英文标题】:Calculating 3D tangent space 【发布时间】:2009-08-29 10:50:33 【问题描述】:

为了在 GLSL 着色器中使用法线贴图,您需要知道每个顶点的法线、切线和双切线向量。 RenderMonkey 为此提供了它自己的预定义变量(rm_tangentrm_binormal),使这变得简单。我正在尝试将此功能添加到我自己的 3d 引擎中。显然,可以使用每个顶点的 xyz 坐标、uv 纹理坐标和法线向量来计算三角形中每个顶点的切线和双切线。经过一番搜索,我设计了这个函数来计算三角形结构中每个顶点的切线和双切线。

void CalculateTangentSpace(void) 
    float x1 = m_vertices[1]->m_pos->Get(0) - m_vertices[0]->m_pos->Get(0);
    float x2 = m_vertices[2]->m_pos->Get(0) - m_vertices[0]->m_pos->Get(0);
    float y1 = m_vertices[1]->m_pos->Get(1) - m_vertices[0]->m_pos->Get(1);
    float y2 = m_vertices[2]->m_pos->Get(1) - m_vertices[0]->m_pos->Get(1);
    float z1 = m_vertices[1]->m_pos->Get(2) - m_vertices[0]->m_pos->Get(2);
    float z2 = m_vertices[2]->m_pos->Get(2) - m_vertices[0]->m_pos->Get(2);

    float u1 = m_vertices[1]->m_texCoords->Get(0) - m_vertices[0]->m_texCoords->Get(0);
    float u2 = m_vertices[2]->m_texCoords->Get(0) - m_vertices[0]->m_texCoords->Get(0);
    float v1 = m_vertices[1]->m_texCoords->Get(1) - m_vertices[0]->m_texCoords->Get(1);
    float v2 = m_vertices[2]->m_texCoords->Get(1) - m_vertices[0]->m_texCoords->Get(1);

    float r = 1.0f/(u1 * v2 - u2 * v1);

    Vec3<float> udir((v2 * x1 - v1 * x2) * r, (v2 * y1 - v1 * y2) * r, (v2 * z1 - v1 * z2) * r);
    Vec3<float> vdir((u1 * x2 - u2 * x1) * r, (u1 * y2 - u2 * y1) * r, (u1 * z2 - u2 * z1) * r);

    Vec3<float> tangent[3];
    Vec3<float> tempNormal;

    tempNormal = *m_vertices[0]->m_normal;
    tangent[0]=(udir-tempNormal*(Vec3Dot(tempNormal, udir)));
    m_vertices[0]->m_tangent=&(tangent[0].Normalize());
    m_vertices[0]->m_bitangent=Vec3Cross(m_vertices[0]->m_normal, m_vertices[0]->m_tangent);

    tempNormal = *m_vertices[1]->m_normal;
    tangent[1]=(udir-tempNormal*(Vec3Dot(tempNormal, udir)));
    m_vertices[1]->m_tangent=&(tangent[1].Normalize());
    m_vertices[1]->m_bitangent=Vec3Cross(m_vertices[1]->m_normal, m_vertices[1]->m_tangent);

    tempNormal = *m_vertices[2]->m_normal;
    tangent[2]=(udir-tempNormal*(Vec3Dot(tempNormal, udir)));
    m_vertices[2]->m_tangent=&(tangent[2].Normalize());
    m_vertices[2]->m_bitangent=Vec3Cross(m_vertices[2]->m_normal, m_vertices[2]->m_tangent);

当我使用这个函数并将计算的值发送到我的着色器时,模型看起来几乎就像它们在 RenderMonkey 中所做的一样,但它们以一种非常奇怪的方式闪烁。我将问题追溯到我发送 OpenGL 的切线和双切线。这让我怀疑我的代码做错了什么。任何人都可以看到任何问题或对其他尝试方法有任何建议吗?

我还应该指出,上面的代码非常hacky,我对发生的事情背后的数学了解很少。

【问题讨论】:

【参考方案1】:

找到了解决办法。更简单(但仍然有点 hacky)的代码:

void CalculateTangentSpace(void) 
    float x1 = m_vertices[1]->m_pos->Get(0) - m_vertices[0]->m_pos->Get(0);
    float y1 = m_vertices[1]->m_pos->Get(1) - m_vertices[0]->m_pos->Get(1);
    float z1 = m_vertices[1]->m_pos->Get(2) - m_vertices[0]->m_pos->Get(2);

    float u1 = m_vertices[1]->m_texCoords->Get(0) - m_vertices[0]->m_texCoords->Get(0);

    Vec3<float> tangent(x1/u1, y1/u1, z1/u1);
    tangent = tangent.Normalize();

    m_vertices[0]->m_tangent = new Vec3<float>(tangent);
    m_vertices[1]->m_tangent = new Vec3<float>(tangent);
    m_vertices[2]->m_tangent = new Vec3<float>(tangent);

    m_vertices[0]->m_bitangent=new Vec3<float>(Vec3Cross(m_vertices[0]->m_normal, m_vertices[0]->m_tangent)->Normalize());
    m_vertices[1]->m_bitangent=new Vec3<float>(Vec3Cross(m_vertices[1]->m_normal, m_vertices[1]->m_tangent)->Normalize());
    m_vertices[2]->m_bitangent=new Vec3<float>(Vec3Cross(m_vertices[2]->m_normal, m_vertices[2]->m_tangent)->Normalize());

【讨论】:

对不起,那是错误的。您所做的只是将边缘 v01 缩放 1/u1。 u1 不仅可以为零,而且显然是不正确的。正确答案请参考***.com/questions/5255806/…【参考方案2】:

对于 u1、u2、v1 和 v2 的某些值,您将在 'r' 计算中得到零除法,从而导致 'r' 的行为未知。你应该提防这种情况。如果分母为零,请弄清楚“r”应该是什么,这可能会解决您的问题。我对这背后的数学也知之甚少。

如果分母为零,则设置 r = 0 的建议实现:

#include <cmath>
...
static float PRECISION = 0.000001f;
...
float denominator = (u1 * v2 - u2 * v1);
float r = 0.f; 
if(fabs(denominator) > PRECISION)     
    r = 1.0f/denominator;

...

【讨论】:

感谢您的回复。不幸的是,您的建议没有帮助。

以上是关于计算 3D 切线空间的主要内容,如果未能解决你的问题,请参考以下文章

(转)D3D11游戏编程学习笔记二十四:切线空间(Tangent Space)

XDRender_LightMode_Anisotropic 各项异性着色

如何保证切线空间矩阵正确

如何保证切线空间矩阵正确

[Unity Shader] 切线空间的法线贴图

使用具有世界空间法线的切线空间法线贴图