如何计算Tangent和Binormal?

时间:2011-03-10 05:23:13

标签: math opengl 3d glsl

在OpenGL着色语言(GLSL)中讨论凹凸贴图,镜面高光和这些东西

我有:

  • 顶点数组(例如{0.2,0.5,0.1,0.2,0.4,0.5,...})
  • 法线数组(例如{0.0,0.0,1.0,0.0,1.0,0.0,...})
  • 点光源在世界空间中的位置(例如{0.0,1.0,-5.0})
  • 观众在世界空间中的位置(例如{0.0,0.0,0.0})(假设观众位于世界的中心)

现在,我如何计算每个顶点的Binormal和Tangent?我的意思是,计算Binormals的公式是什么,我必须根据这些信息使用什么?关于切线?

无论如何我都会构建TBN矩阵,所以如果你知道根据这些信息直接构造矩阵的公式会很好!

哦,是的,如果需要,我也有纹理坐标。 当我在谈论GLSL时,我认为,每个顶点解决方案都是不错的,它不需要一次访问多个顶点信息。

----更新-----

我找到了这个解决方案:

vec3 tangent;
vec3 binormal;

vec3 c1 = cross(a_normal, vec3(0.0, 0.0, 1.0));
vec3 c2 = cross(a_normal, vec3(0.0, 1.0, 0.0));

if (length(c1)>length(c2))
{
    tangent = c1;
}
else
{
    tangent = c2;
}

tangent = normalize(tangent);

binormal = cross(v_nglNormal, tangent);
binormal = normalize(binormal);

但我不知道它是否100%正确。

4 个答案:

答案 0 :(得分:37)

问题的相关输入数据是纹理坐标。 Tangent和Binormal是局部平行于对象表面的向量。在正常映射的情况下,它们描述了正常纹理的局部方向。

因此,您必须计算纹理向量指向的方向(在模型的空间中)。假设您有一个三角形ABC,纹理坐标为HKL。这给了我们矢量:

D = B-A
E = C-A

F = K-H
G = L-H

现在我们想用切线空间T,U表示D和E,即

D = F.s * T + F.t * U
E = G.s * T + G.t * U

这是一个具有6个未知数和6个方程的线性方程组,可以写为

| D.x D.y D.z |   | F.s F.t | | T.x T.y T.z |
|             | = |         | |             |
| E.x E.y E.z |   | G.s G.t | | U.x U.y U.z |

反转FG矩阵产生

| T.x T.y T.z |           1         |  G.t  -F.t | | D.x D.y D.z |
|             | = ----------------- |            | |             |
| U.x U.y U.z |   F.s G.t - F.t G.s | -G.s   F.s | | E.x E.y E.z |

与顶点法线T和U一起形成一个局部空间基础,称为切线空间,由矩阵描述

| T.x U.x N.x |
| T.y U.y N.y |
| T.z U.z N.z |

从切线空间转换为对象空间。要进行照明计算,需要与此相反。通过一点运动,人们发现:

T' = T - (N·T) N
U' = U - (N·U) N - (T'·U) T'

对矢量T'和U'进行归一化,将它们称为切线和二次正规,我们得到矩阵从物体转换为切线空间,我们在那里进行照明:

| T'.x T'.y T'.z |
| U'.x U'.y U'.z |
| N.x  N.y  N.z  |

我们将T'和U'与顶点法线一起存储为模型几何的一部分(作为顶点属性),以便我们可以在着色器中使用它们进行光照计算。 我再说一遍:你没有在着色器中确定切线和副法线,你预先计算它们并将它们存储为模型几何体的一部分(就像法线一样)。

(上面竖条之间的符号都是矩阵,从不是决定因素,通常在表示法中使用竖线代替括号。)

答案 1 :(得分:15)

通常,您有两种生成TBN矩阵的方法:离线和在线。

    使用衍生指令在片段着色器中
  • 在线 =右。这些推导为多边形的每个点提供了平坦的TBN基础。为了得到平滑的,我们必须基于给定的(平滑的)顶点法线重新正交化它。这个过程在GPU上比初始TBN提取更加沉重。

    // compute derivations of the world position
    vec3 p_dx = dFdx(pw_i);
    vec3 p_dy = dFdy(pw_i);
    // compute derivations of the texture coordinate
    vec2 tc_dx = dFdx(tc_i);
    vec2 tc_dy = dFdy(tc_i);
    // compute initial tangent and bi-tangent
    vec3 t = normalize( tc_dy.y * p_dx - tc_dx.y * p_dy );
    vec3 b = normalize( tc_dy.x * p_dx - tc_dx.x * p_dy ); // sign inversion
    // get new tangent from a given mesh normal
    vec3 n = normalize(n_obj_i);
    vec3 x = cross(n, t);
    t = cross(x, n);
    t = normalize(t);
    // get updated bi-tangent
    x = cross(b, n);
    b = cross(n, x);
    b = normalize(b);
    mat3 tbn = mat3(t, b, n);
    
  • 离线 =准备切线作为顶点属性。这更难以获得,因为它不仅会添加另一个顶点属性,还需要重新组合所有其他属性。此外,它不会100%为您提供更好的性能,因为您将获得存储/传递/动画(!)vector3顶点属性的额外成本。

数学在很多地方都有描述(google it),包括@datenwolf帖子。

这里的问题是2个顶点可能具有相同的法线和纹理坐标但是不同的切线。这意味着您不能仅将顶点属性添加到顶点,您需要将顶点拆分为2并为克隆指定不同的切线。

获得每个顶点的唯一切线(和其他属性)的最佳方法是尽可能早地在导出器中执行此操作。在按属性排序纯顶点的阶段,您只需将切线向量添加到排序键。

作为问题的根本解决方案,请考虑使用四元数。单个四元数(vec4)可以成功地表示预定义的手感的切向空间。保持正交(包括传递给片段着色器)很容易,如果需要,存储和提取正常。有关KRI wiki的更多信息。

答案 2 :(得分:0)

根据kvark的回答,我想补充更多想法。

如果您需要正交归一化切线空间矩阵,则必须以任何方式完成某些工作。 即使您添加了切线和二进制属性,它们也会在着色器阶段进行插值 最后,他们既没有正常化,也没有彼此正常。

我们假设我们有规范化的normalvector n,我们有正切t和正常b或者我们可以计算它们的推导如下:

// derivations of the fragment position
vec3 pos_dx = dFdx( fragPos );
vec3 pos_dy = dFdy( fragPos );
// derivations of the texture coordinate
vec2 texC_dx = dFdx( texCoord );
vec2 texC_dy = dFdy( texCoord );
// tangent vector and binormal vector
vec3 t = texC_dy.y * pos_dx - texC_dx.y * pos_dy;
vec3 b = texC_dx.x * pos_dy - texC_dy.x * pos_dx;

当然,正交归一化切线空间矩阵可以通过使用叉积来计算, 但这只适用于右手系统。如果矩阵被镜像(左手系统),它将转向右手系统:

t = cross( cross( n, t ), t ); // orthonormalization of the tangent vector
b = cross( n, t );             // orthonormalization of the binormal vector 
                               //   may invert the binormal vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

在上面的代码片段中,如果切线空间是左手系统,则反转副法向量。 要避免这种情况,必须采取艰难的方式:

t = cross( cross( n, t ), t ); // orthonormalization of the tangent vector
b = cross( b, cross( b, n ) ); // orthonormalization of the binormal vectors to the normal vector 
b = cross( cross( t, b ), t ); // orthonormalization of the binormal vectors to the tangent vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

正交任何矩阵的常用方法是Gram–Schmidt process

t = t - n * dot( t, n ); // orthonormalization ot the tangent vectors
b = b - n * dot( b, n ); // orthonormalization of the binormal vectors to the normal vector 
b = b - t * dot( b, t ); // orthonormalization of the binormal vectors to the tangent vector
mat3 tbn = mat3( normalize(t), normalize(b), n );

另一种可能性是使用2 * 2矩阵的行列式,其由纹理坐标texC_dxtexC_dy的推导得出,以考虑副法线向量的方向。这个想法是正交矩阵的行列式是1和确定的正交镜矩阵-1。

决定因素可以通过GLSL函数计算determinant( mat2( texC_dx, texC_dy ) 或者可以用公式texC_dx.x * texC_dy.y - texC_dy.x * texC_dx.y来计算。

对于正交归一化切线空间矩阵的计算,不再需要副法线向量并计算单位向量 可以避免(normalize)副法向量。

float texDet = texC_dx.x * texC_dy.y - texC_dy.x * texC_dx.y;
vec3 t = texC_dy.y * pos_dx - texC_dx.y * pos_dy;
t      = normalize( t - n * dot( t, n ) );
vec3 b = cross( n, t );                      // b is normlized because n and t are orthonormalized unit vectors
mat3 tbn = mat3( t, sign( texDet ) * b, n ); // take in account the direction of the binormal vector

答案 3 :(得分:0)

有多种计算切线的方法,如果法线贴图面包师的计算方式与渲染器不同,您将获得细微的伪影。许多面包师使用 MikkTSpace 算法,这与片段导数技巧不同。

幸运的是,如果您有一个使用 MikkTSpace 的程序的索引网格(并且没有具有相反方向的纹理坐标三角形共享索引)算法的困难部分主要为您完成,您可以像这样重建切线:

#include <cmath>
#include "glm/geometric.hpp"
#include "glm/vec2.hpp"
#include "glm/vec3.hpp"
#include "glm/vec4.hpp"

using glm::vec2;
using glm::vec3;
using glm::vec4;

void makeTangents(uint32_t nIndices, uint16_t* indices,
                  const vec3 *positions, const vec3 *normals,
                  const vec2 *texCoords, vec4 *tangents) {
  uint32_t inconsistentUvs = 0;
  for (uint32_t l = 0; l < nIndices; ++l) tangents[indices[l]] = vec4(0);
  for (uint32_t l = 0; l < nIndices; ++l) {
    uint32_t i = indices[l];
    uint32_t j = indices[(l + 1) % 3 + l / 3 * 3];
    uint32_t k = indices[(l + 2) % 3 + l / 3 * 3];
    vec3 n = normals[i];
    vec3 v1 = positions[j] - positions[i], v2 = positions[k] - positions[i];
    vec2 t1 = texCoords[j] - texCoords[i], t2 = texCoords[k] - texCoords[i];

    // Is the texture flipped?
    float uv2xArea = t1.x * t2.y - t1.y * t2.x;
    if (std::abs(uv2xArea) < 0x1p-20)
      continue;  // Smaller than 1/2 pixel at 1024x1024
    float flip = uv2xArea > 0 ? 1 : -1;
    // 'flip' or '-flip'; depends on the handedness of the space.
    if (tangents[i].w != 0 && tangents[i].w != -flip) ++inconsistentUvs;
    tangents[i].w = -flip;

    // Project triangle onto tangent plane
    v1 -= n * dot(v1, n);
    v2 -= n * dot(v2, n);
    // Tangent is object space direction of texture coordinates
    vec3 s = normalize((t2.y * v1 - t1.y * v2)*flip);
    
    // Use angle between projected v1 and v2 as weight
    float angle = std::acos(dot(v1, v2) / (length(v1) * length(v2)));
    tangents[i] += vec4(s * angle, 0);
  }
  for (uint32_t l = 0; l < nIndices; ++l) {
    vec4& t = tangents[indices[l]];
    t = vec4(normalize(vec3(t.x, t.y, t.z)), t.w);
  }
  // std::cerr << inconsistentUvs << " inconsistent UVs\n";
}

在顶点着色器中,它们被旋转到世界空间:

  fragNormal = (model.model * vec4(inNormal, 0)).xyz;
  fragTangent = vec4((model.model * vec4(inTangent.xyz, 0)).xyz, inTangent.w);

然后像这样计算副法线和世界空​​间法线(见http://mikktspace.com/):

  vec3 binormal = fragTangent.w * cross(fragNormal, fragTangent.xyz);
  vec3 worldNormal = normalize(normal.x * fragTangent.xyz +
                               normal.y * binormal +
                               normal.z * fragNormal);

(副法线通常按像素计算,但有些面包师会为您提供按顶点计算并进行插值的选项。This page 包含有关特定程序的信息。)