Skip to content

OpenGL 为什么法线矩阵定义为“模型矩阵左上角 3x3 部分的逆矩阵的转置”

在 3D 渲染中,光照计算离不开物体表面的法线(Normal)。然而,当对物体应用模型变换矩阵(Model Matrix)时,直接用该矩阵去变换法线往往会得到错误的结果。为了保证法线在变换后依然与表面垂直,我们需要使用一个特殊的矩阵——法线矩阵(Normal Matrix),其定义为模型矩阵左上角 部分的逆矩阵的转置(Transpose of the Inverse Matrix)

本文将从数学推导、直观几何意义以及实际渲染性能优化的角度,详细解释这一设计的底层逻辑。


一、 法线变换的核心冲突

法线是一个方向向量,其根本属性是垂直于物体的表面(更精确地讲,是垂直于该点切线所在的空间平面)。

1. 旋转与平移变换

  • 平移(Translation):法线代表方向,没有位置概念,因此平移不应影响法线。在齐次坐标中,我们将法线的 分量设为 0.0 即可过滤平移。
  • 均匀旋转(Rotation):当物体整体旋转时,表面和法线会同步旋转相同的角度。这种情况下,直接用模型矩阵的旋转部分(左上角 )乘以法线是完全正确的。

2. 核心问题:非均匀缩放(Non-uniform Scaling)

当物体在不同轴向上的缩放比例不一致时,直接使用模型矩阵变换法线就会导致法线偏离垂直方向

text
【 非均匀缩放对法线的影响示意图 】

原始三角形 (等腰直角):

      Y
      ^
      |
    C + (0,1)
      | \
      |   \          <-- 法线 N (0.7, 0.7)
      +-----+------> X
    A(0,0)  B(1,0)

在 X 轴拉伸 2 倍后:

      Y
      ^
      |
    C + (0,1)
      |  \
      |    \         <-- 错误法线 N' (1.4, 0.7)(不再垂直!正确法线应该更倾向于 Y 轴!)
      +------+---------> X
    A(0,0)   B'(2,0)

如上图所示:

  • 原始斜边的切线方向为 ,法线为 ,点积为 (正交)。
  • 如果在 X 轴缩放 2 倍,斜边上的点 变为 ,新斜边切线方向变为
  • 若直接对原法线 乘以相同的缩放矩阵(X 轴乘 2),得到的新法线为
  • 验证正交性:新切线与新法线的点积为 法线不再垂直于表面!

为了修正这种由非均匀缩放导致的畸变,必须推导出一个能让变换后的法线依然垂直于新切线的矩阵。


二、 严密的数学推导

我们采用线性代数中的标准列向量记法进行推导。

设在物体表面某点处:

  • 为该点处的一个切线向量(Tangent Vector)
  • 为该点处的法线向量(Normal Vector)

由于法线垂直于表面切线,它们的点积为零。写成矩阵乘法形式(列向量的内积为 ):

现在我们对物体施加模型变换矩阵 (在此指代去除平移后的左上角 矩阵):

  • 切线的变换:切线是由表面上两个无限接近的顶点相减得到的方向向量。因为顶点是由 变换的(),所以切线也必须直接使用 进行变换:
  • 法线的变换:假设我们寻找的法线变换矩阵为 ,则变换后的法线为:

为了保证变换后的法线 依然与新切线 垂直,必须满足:

代入上式:

根据矩阵转置的性质 ,展开左侧乘积:

对比初始条件 。要使上式对任意切线 和法线 都恒成立,中间的矩阵乘积 必须为单位矩阵

解方程求出法线变换矩阵

结论:用于变换法线的矩阵 必须是模型矩阵 的逆矩阵的转置。这就是“逆矩阵的转置”的数学来源。


三、 几何直观理解

为什么“逆矩阵的转置”能纠正缩放导致的倾斜?

  1. 逆矩阵(Inverse)的作用
    • 逆矩阵代表的是相反的变换
    • 如果我们在 X 轴方向将物体拉伸了 2 倍(缩放因子为 2),那么在逆矩阵中,X 轴方向的缩放因子就变成了
    • 这非常符合几何直观:当表面在 X 方向被拉宽、坡度变缓时,法线为了保持垂直,应该向 Y 轴方向倾斜,也就是在 X 方向收缩。
  2. 转置矩阵(Transpose)的作用
    • 转置操作保证了向量在正交投影下的乘法方向正确,将“行向量式”的受力分布重新映射回列向量。

四、 两种特殊情况下的简化

由于求逆矩阵(Inverse)在计算机中是比较昂贵的操作,在实际开发中,我们不需要对所有物体都计算逆转置。

情况 1:纯旋转和平移(无缩放)

如果模型矩阵只包含旋转和平移,那么其左上角的 矩阵 是一个正交矩阵(Orthogonal Matrix)。 根据正交矩阵的定义,其转置矩阵等于逆矩阵:

优化结论:当物体没有缩放时,法线变换矩阵就是 本身。我们可以直接使用模型矩阵的 部分乘以法线。

情况 2:包含均匀缩放(Uniform Scaling)

如果物体在 X、Y、Z 轴上的缩放因子相同(设为 ),则 为旋转矩阵)。它的逆转置矩阵为:

优化结论:均匀缩放只会改变变换后法线向量的长度(乘以系数 ),而不会改变其方向。因此,我们依然可以直接用 部分来变换法线,只需在变换后将法线重新**归一化(Normalize)**即可。


五、 GLSL 代码应用与性能优化

在编写 Shader 时,计算法线矩阵有两种选择。

方案 A:在顶点着色器(VS)中动态计算(不推荐)

glsl
# version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

uniform mat4 model;

out vec3 Normal;

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    
    // ❌ 不推荐:在 GPU 中为每个顶点重复计算矩阵的逆和转置,极度浪费算力
    mat3 normalMatrix = transpose(inverse(mat3(model)));
    Normal = normalize(normalMatrix * aNormal);
}

方案 B:在 CPU 中计算并作为 Uniform 传入(推荐最佳实践)

因为一个 Mesh 的 Model 矩阵在一帧绘制中是恒定的,所以法线矩阵应该在 CPU 端计算完毕,然后直接传给 GPU。

C++ 端的计算(以 GLM 为例)

cpp
# include <glm/glm.hpp>
# include <glm/gtc/matrix_transform.hpp>

glm::mat4 modelMatrix = getModelMatrix(); // 获取当前物体的 model 矩阵

// 1. 提取左上角 3x3 矩阵,求逆并求转置
glm::mat3 normalMatrix = glm::transpose(glm::inverse(glm::mat3(modelMatrix)));

// 2. 传给 Shader
shader.setMat3("normalMatrix", normalMatrix);

GLSL 顶点着色器

glsl
# version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

uniform mat4 model;
uniform mat3 normalMatrix; // 直接接收 CPU 计算好的法线矩阵

out vec3 Normal;

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    // 直接相乘,性能最优
    Normal = normalize(normalMatrix * aNormal);
}

六、 总结

问题物理/数学本质解决方案
为什么普通变换矩阵不行?非均匀缩放会破坏切线与法线的垂直夹角。必须引入与原缩放相反方向的变换。
为什么是“逆矩阵的转置”?基于 推导出 ,从而求得 完美保持正交性。
如何优化性能?逆矩阵计算量大,避免在 GPU 逐顶点计算。在 CPU 算好后以 uniform mat3 形式送入 Shader。

基于 VitePress 强力驱动 | 记录技术与生活