法线矩阵(Normal Matrix)和TBN矩阵

文章介绍了在OpenGL中处理法线向量的重要性,特别是在环境映射和光照计算中。法线矩阵,即模型-观测矩阵的逆转置,用于确保法线在变换后仍垂直于表面。文章讨论了错误地直接使用模型-观测矩阵变换法线的问题,以及为何需要转置和逆矩阵。此外,还讲解了TBN空间的概念,它是由切线、副切线和法线构成的局部坐标系统,用于处理法线贴图,TBN矩阵用于在不同空间之间转换法线向量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

呃呃,在看learnopengl的立方体贴图那张,做环境映射的时候,需要用到世界空间中的法线信息,但是很离谱,我又忘了,这里直接转载一下别的大佬的文章,作为自己的记录,下次忘了就直接翻自己的博客,文章末尾放了引用的文章链接。

0. 背景介绍

很多 顶点着色器 ( vertex shader) 中都用到法线矩阵 ( normal matrix )。本文内容涉及法线矩阵是什么、法线矩阵有什么用。

有很多计算工作是在 观测空间 ( eye space ) 下完成的,其中包括与光照相关的计算。如果不在观测空间计算,与观测位置相关的效果将很难实现,如高光 ( specular )。

因此,我们需要一种方法,将法线转换到观测空间。将顶点变换到观测空间的计算,可以写成:

vertexEyeSpace = gl_ModelViewMatrix * gl_Vertex;

那为什么不能对法线做一遍同样的操作呢?法线是有 3 个浮点数分量的向量,模型-观测矩阵 是 4x4 的矩阵。法线是一个向量,我们只想改变其方向。模型-观测矩阵 左上区域的 3x3 矩阵包含改变方向的子矩阵,那我们为什么不直接用法向量左乘这个子矩阵?

1.错误的做法与分析

这很容易通过下面的代码实现:

normalEyeSpace = vec3(gl_ModelViewMatrix * vec4(gl_Normal, 0.0f));

所以,gl_NormalMatrix 只是一个简化或优化代码编写的捷径?不,不是的。上面一行代码只在某些情况下有效。

让我们看看潜在的问题:
在这里插入图片描述
上图中有一个三角形、一条法向量、一条切向量。下图将展示当 模型-观测矩阵 包含非均匀的缩放时,会发生什么。
在这里插入图片描述
注意:如果缩放是均匀的,法线的方向将保持不变,长度会受影响,但很容易通过单位化修复

如果上图的 模型-观测矩阵 被应用到所有顶点和法线,显然会得到错误的结果:法线将不再垂直于平面。

我们知道,向量可以用两个点的差表示。比如切向量,可以通过三角形边上的两个顶点做差得到。如果 P 1 P_1 P1 P 2 P_2 P2 就是定义在三角形边上的两个顶点,可以得到:
在这里插入图片描述
考虑到在齐次坐标中,向量可以用含四个分量的元组表示 ( 最后一个分量为 0 ),可以让等号两侧同时左乘 模型-观测矩阵:
在这里插入图片描述
化简成:
在这里插入图片描述
因为 P 1 ′ P'_1 P1 P 2 ′ P'_2 P2 是变换后三角形边上的顶点,所以 T ′ T' T 仍然是三角形边上的切向量,故可以认为:模型-变换矩阵 保留了切向量,却没有保留法向量。

2. 改进与推导

考虑对向量 T ′ ⃗ \vec{T'} T 采用的方法,我们可以假设两个顶点,如下:
在这里插入图片描述
主要的问题如之前图中展示的那样,通过变换后的点定义的向量 Q 2 ′ Q'_2 Q2- Q 1 ′ Q'_1 Q1 不一定保持原样 ( 原来是垂直于三角形面的 )。法向量不像切向量那样,通过两个点做差定义,而只定义为一个垂直于平面的向量。

所以,我们明白了,不能简单地把 模型-观测矩阵 应用于所有情况下的法向量。那问题来了,我们应该用啥样的矩阵呢?

考虑一个 3x3 的矩阵 G ,然后我们来看看这个矩阵如何计算,并完美地转换法向量。

在变换前后, T ⃗ \vec{T} T N ⃗ \vec{N} N 都是垂直的,因此在变换前,满足
在这里插入图片描述
变换后还满足
在这里插入图片描述

切向量 T ⃗ \vec{T} T 可以安全地左乘 模型-观测矩阵 左上方的 3x3 子矩阵 ( T ⃗ \vec{T} T 是一个齐次坐标下的向量,w 分量为 0 ),我们把这个子矩阵称为 M M M

假设矩阵 G 能正确的转换法向量 N ⃗ \vec{N} N ,得出等式:
在这里插入图片描述
向量点积等价于对应分量乘积之和,得:
在这里插入图片描述
在这里插入图片描述
注意:第一个向量必须转置,以便计算对应分量乘积之和

我们还知道,乘法的转置,就是转置的乘法,即:
在这里插入图片描述
所以:
在这里插入图片描述
首先声明 N ⃗ \vec{N} N · T ⃗ \vec{T} T = 0 ,所以如果有:
在这里插入图片描述
那就能满足我们的声明:
在这里插入图片描述

所以通过 M 反推出 G :
在这里插入图片描述

3. 总结

因此,能正确转换法向量的矩阵,就是 M M M 的逆的转置。OpenGL 在 gl_NormalMatrix 中进行这步计算。

在本节开始提到,在某些情况下,模型-变换矩阵 可以直接应用于法向量的变换,即当 模型-观测矩阵 是正交矩阵时:
在这里插入图片描述

对于正交矩阵,其转置等于其逆。那什么是正交矩阵呢?

  • 一个正交矩阵,任意的行/列都是单位长度,且互相垂直。
  • 两个向量分别乘正交矩阵后的夹角,与变换前是一致的。
  • 简单地说,变换保留了向量之间的角度关系,因此变换后的法线与切线仍互相垂直!此外,还保留了向量的长度。

那如何确定 M 是正交矩阵呢?

  • 当我们的变换中,只包含旋转和平移,即在 OpenGL 中,我们只使用 glRotate 和 glTranslate,而不使用 glScale。这样操作能保证 M 是正交的。注意: gluLookAt 也创建了一个正交矩阵!

4. 例子GLSL:用transpose和inverse函数转换顶点着色器里的法线向量

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
 
out vec3 FragPos;
out vec3 Normal;
 
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
 
void main()
{
    FragPos = vec3(model * vec4(aPos, 1.0)); 
    Normal = mat3(transpose(inverse(model) ) )  * aNormal;
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

为什么要转换顶点着色器里的法线向量?

  • 为了确定法线向量在顶点着色器里的方向始终垂直于表面。
  • 如果模型矩阵进行非一致性(non-uniform)缩放(scale),顶点变化后,其法线向量不再垂直于表面,所以需要法线矩阵
  • 法线矩阵是 乘 法线向量,可把该法线转化为即使缩放也不会不垂直于表面的法线向量
  • 法线矩阵可从模型视图矩阵转换而来(转置逆转矩阵),但因为是工作在世界空间(而不是视图空间),所以用模型矩阵转换
  • 把模型矩阵转化为3x3矩阵(用mat3)失去转置属性,而后可和vec3向量相乘

新增:TBN空间和TBN矩阵

在这里插入图片描述

  • 切线空间定义于每一个顶点之中,是由切线( T a n g e n t Tangent Tangent),副切线( B i T a n g e n t BiTangent BiTangent),顶点法线( N o r m a l Normal Normal)以模型顶点为中心的坐标空间。
  • n o r m a l M a p normalMap normalMap 中的法向量在切空间中表示,其中法向量总是大致指向正z方向。
  • 切线空间是一个三角形表面的局部空间:法线相对于单个三角形的局部参考系。把它想象成法向量的局部空间;它们都是指向正z方向的不管最终变换的方向是什么。
  • 使用一个特定的矩阵,我们可以将这个局部切线空间的法向量转换为世界或视图坐标,并将它们沿最终映射曲面的方向定向。这个矩阵就是 T B N TBN TBN 矩阵。接下来将详细推导 T B N TBN TBN 矩阵的构造过程。
    在这里插入图片描述只需要下面两个步骤即可得到规范化的 T B N TBN TBN 矩阵:
    在这里插入图片描述
    E 1 = Δ U 1 T + Δ V 1 B E 2 = Δ U 2 T + Δ V 2 B E_1 = \Delta U_1T + \Delta V_1B \\ E_2 = \Delta U_2T + \Delta V_2B E1=ΔU1T+ΔV1BE2=ΔU2T+ΔV2B

该公式的数学意义是,如何将一个点从uv空间映射到三维空间,其中TB作为基矢量,以uv空间中u和v的增长作为控制参数,假设三角形中存在一点p,则 A P ⃗ = u ( p ) ∗ T ⃗ + v ( p ) ∗ B ⃗ \vec {AP}=u(p) * \vec T + v(p) * \vec B AP =u(p)T +v(p)B , (点p可以表示为以TB为基矢量的uv空间,TB轴的线性组合,在这里A是UV坐标原点)。

根据以上公式可以快速的推导出TB:
T ⃗ = Δ V 1 E 2 − Δ V 2 E 1 Δ V 1 Δ U 2 − Δ V 2 Δ U 1   B ⃗ = − Δ U 1 E 2 + Δ U 2 E 1 Δ V 1 Δ U 2 − Δ V 2 Δ U 1 \vec T = \frac {\Delta V_1E_2 - \Delta V_2E_1}{\Delta V_1 \Delta U_2 - \Delta V_2 \Delta U1}\\ \space \\ \vec B = \frac {- \Delta U_1E_2 + \Delta U_2E_1}{\Delta V_1 \Delta U_2 - \Delta V_2 \Delta U1} T =ΔV1ΔU2ΔV2ΔU1ΔV1E2ΔV2E1 B =ΔV1ΔU2ΔV2ΔU1ΔU1E2+ΔU2E1
目前给出的TB还是不是真正的切线与副切线,需要正交化后得到 T B N TBN TBN 矩阵:
t ⊥ ⃗ = n o r m a l i z e d ( t ⃗ − ( t ⃗ ⋅ n ⃗ ) n ⃗ )   b ⊥ ⃗ = n o r m a l i z e d ( b ⃗ − ( b ⃗ ⋅ n ⃗ ) n ⃗ − ( b ⃗ ⋅ t ⊥ ⃗ ) t ⊥ ) \vec {t_{\perp}} = normalized(\vec t - (\vec t · \vec n)\vec n)\\ \space \\ \vec {b_{\perp}} = normalized(\vec b - (\vec b · \vec n)\vec n - (\vec b · \vec {t_{\perp}}) t_{\perp})\\ t =normalized(t (t n )n ) b =normalized(b (b n )n (b t )t)

  • t ⃗ \vec t t 是正交前的切线方向,也可以说是u方向。
  • b ⃗ \vec b b 是正交前的副切线方向,也可以说是v方向。
  • t ⊥ ⃗ \vec {t_{\perp}} t b ⊥ ⃗ \vec {b_{\perp}} b 表示的是正交后的切线方向和正交后的副切线方向。正交后的切线和副切线才能算作是切向空间的坐标轴。
  • 切向空间是由切线 t ⊥ ⃗ \vec {t_{\perp}} t 、副切线 b ⊥ ⃗ \vec {b_{\perp}} b 、顶点法线 n ⃗ \vec n n 以模型顶点为中心的坐标空间。也就是说切线空间的三个坐标轴就是( t ⊥ ⃗ \vec {t_{\perp}} t b ⊥ ⃗ \vec {b_{\perp}} b n ⃗ \vec n n )。
  • (另一种理解:其中n是建模软件中规定的顶点法线,可以看到n在正交化过程中不会受到影响,该过程是对TB向量进行方向的调整以及长度的归一化。TB在此过程后会相互垂直,此时将不再一定与UV方向保持相同。特别的,当调整顶点法线后,TB平面甚至将与三维空间中的三角形平面不同,也就是说归正交化后的 T B N TBN TBN 矩阵, T B TB TB 轴将不再与uv相等,uv是正交化前的 T B TB TB 轴。)

通过正交化后的 T a n g e n t ( T ) Tangent(T) Tangent(T) B i t a n g e n t ( B ) Bitangent(B) Bitangent(B) N o r m a l ( N ) Normal(N) Normal(N) 可以推导出 T B N TBN TBN 矩阵:
在这里插入图片描述
n o r m a l M a p normalMap normalMap 中存储的法线信息是基于 T B N TBN TBN 空间的,而光照计算需要所有的参数在同一空间下,以上计算出的 T B N TBN TBN 矩阵就是用于实现将 T B N TBN TBN 空间中定义的法线转换到世界空间。
在这里插入图片描述
根据矩阵的逆的性质, T B N TBN TBN 矩阵的逆矩阵可以用来将矢量从世界空间转换到 T B N TBN TBN 空间中,而 T B N TBN TBN 矩阵是正交化过的,根据正交矩阵的特殊性质(正交矩阵的逆等于其转置),可以轻松求得 T B N TBN TBN 的逆矩阵:
在这里插入图片描述

TBN矩阵注意点

这里还有一些需要记住的东西:

  • 法线贴图中的法线向量是定义在切线空间中。
  • TBN矩阵是切线空间和其他空间的转换矩阵。
  • TBN矩阵是正交矩阵,即 ( T B N ) T = = ( T B N ) − 1 (TBN)^T == (TBN)^{-1} (TBN)T==(TBN)1 ,矩阵的转置=矩阵的逆 。
  • Unity的mul函数对矩阵和向量的运算做了不同的处理,以 float3x3矩阵 和 float3向量 为例:
    • mul(向量, 矩阵)—— 行向量*矩阵,得行向量。
    • mul(矩阵, 向量)——矩阵*列向量,得列向量。
  • Unity的TBN矩阵(先行再列)在 mul函数的参数顺序:
    • 其他空间 → \rightarrow 切向空间:TBN矩阵在前,如 lDirTS = mul(TBN_OS, lDirOS);
    • 切向空间 → \rightarrow 其他空间(法线贴图使用):TBN矩阵在后,如 nDirWS = mul(nDirTS, TBN_WS)。
  • UnityShader的矩阵组成是先行再列的,所以Unity的TBN矩阵为
    [ T x T y T z B x B y B z N x N y N z ] \begin{bmatrix} T_x&T_y&T_z\\ B_x&B_y&B_z\\ N_x&N_y&N_z \end{bmatrix} TxBxNxTyByNyTzBzNz

引用

用的别的大佬的,下面放一下链接:

### TBN Algorithm in Computer Graphics Normal Mapping TBN (Tangent-Bitangent-Normal)矩阵是一种用于法线贴图的技术,在计算机图形学中广泛应用于提高表面细节的表现力。通过构建局部坐标系,该技术能够将模型空间中的光照计算转换到切线空间下进行操作。 #### 切线空间与法线贴图 为了实现更精细的材质表现效果,通常会利用法线贴图来模拟微小几何结构的变化。然而,由于法线贴图是在物体表面上定义的方向场,因此需要将其从纹理空间变换至世界空间或视图空间以便于光照计算[^1]。这一过程依赖于由顶点数据预先计算得到的TBN基底向量集合——即切线(Tangent),副法线(Bitangent/Binormal),以及法线(Normal)[^2]。 #### 构建TBN Matrix 具体来说,对于每一个三角形网格上的顶点v_i, 都要分别求解其对应的三个正交单位矢量t,b,n: - **Normal Vector n**: 这是从标准几何形状继承而来的原始表面朝向外侧方向; - **Tangent Vector t**: 它位于当前多边形平面内并与u-v参数化映射轴平行; - **Bitangent/ Binormal b**: 可视为上述两者叉乘所得结果,确保最终形成右手定则下的三维直角框架体系。 这些值一般存储在mesh文件当中或者动态生成,并经过插值得应用在整个fragment之上。随后组合成如下形式的3×3旋转矩阵: \[ \text{Matrix}_{\text{TBN}} = \begin{bmatrix} tx & ty & tz \\ bx & by & bz\\ nx & ny & nz \end{bmatrix} \] 此矩阵的作用就是把来自normal map里的信息投影回world space里去参与后续渲染管线内的各项运算处理流程之中. ```glsl // GLSL Shader Code Example for Transforming Normals Using TBN Matrix vec3 tangentSpaceNormal = texture(normalMap, TexCoords).rgb; tangentSpaceNormal = normalize(tangentSpaceNormal * 2.0 - 1.0); mat3 TBN = mat3(Tangent, Bitangent, Normal); vec3 worldSpaceNormal = normalize(TBN * tangentSpaceNormal); ``` 以上展示了如何在一个片段着色器程序内部完成基于已知TBN矩阵情况下来调整输入normals的过程实例代码段落.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值