opengl 如何计算切线和副法线?

vatpfxk5  于 2023-06-22  发布在  其他
关注(0)|答案(4)|浏览(203)

介绍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?我的意思是,计算双正态量的公式是什么,我必须根据这些信息使用什么?关于切线?
我会构造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%正确。

2hh7jdfx

2hh7jdfx1#

与问题相关的输入数据是纹理坐标。切线和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'与顶点法线一起存储为模型几何的一部分(作为顶点属性),以便我们可以在着色器中使用它们进行照明计算。我重复一遍:您不需要在着色器中确定切线和副法线,而是预先计算它们并将其存储为模型几何体的一部分(就像法线一样)。
(The上面竖线之间的符号都是矩阵,而不是行列式,它们通常在符号中使用竖线而不是括号。

lndjwyie

lndjwyie2#

一般来说,有两种生成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);

*Off-line=准备切线作为顶点属性。这是更难得到的,因为它不仅会添加另一个顶点属性,而且还需要重新组合所有其他属性。此外,它不会100%给予你一个更好的性能,因为你会得到额外的成本存储/传递/动画(!)vector 3顶点属性。

很多地方(谷歌一下)都描述了这个数学,包括@datenwolf的帖子。
这里的问题是,两个顶点可能具有相同的法线和纹理坐标,但切线不同。这意味着你不能只给一个顶点添加一个顶点属性,你需要把顶点分成2个,并为克隆指定不同的切线。
获得每个顶点唯一切线(和其他属性)的最佳方法是在导出器中尽早完成。在按属性对纯顶点进行排序的阶段,您只需要将切向量添加到排序键。
作为这个问题的激进解决方案,考虑使用四元数。单个四元数(vec 4)可以成功地表示预定义便利性的切向空间。它很容易保持正交(包括传递到片段着色器),存储和提取正常,如果需要的话。关于KRI wiki的更多信息。

7bsow1i6

7bsow1i63#

基于kvark的回答,我想补充更多的想法。
如果你需要一个正交化的切空间矩阵,你必须做一些工作,无论如何。即使添加切线和副法线属性,它们也将在着色器阶段进行插值,并且在结束时它们既不归一化也不彼此垂直。
假设我们有一个归一化法线向量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
7nbnzgx9

7nbnzgx94#

有多种方法可以计算切线,如果法线贴图烘焙器的计算方式与渲染器不同,您将得到细微的瑕疵。许多面包师使用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);

(The副法线通常是按像素计算的,但有些面包师给予你选择按顶点计算并进行插值。This page有关于特定程序的信息。)

相关问题