OpenGL 为什么法线矩阵定义为“模型矩阵左上角 3x3 部分的逆矩阵的转置
OpenGL 为什么法线矩阵定义为“模型矩阵左上角 3x3 部分的逆矩阵的转置”在 3D 渲染中光照计算离不开物体表面的法线Normal。然而当对物体应用模型变换矩阵Model Matrix时直接用该矩阵去变换法线往往会得到错误的结果。为了保证法线在变换后依然与表面垂直我们需要使用一个特殊的矩阵——法线矩阵Normal Matrix其定义为模型矩阵左上角3×33\times33×3部分的逆矩阵的转置Transpose of the Inverse Matrix。本文将从数学推导、直观几何意义以及实际渲染性能优化的角度详细解释这一设计的底层逻辑。一、 法线变换的核心冲突法线是一个方向向量其根本属性是垂直于物体的表面更精确地讲是垂直于该点切线所在的空间平面。1. 旋转与平移变换平移Translation法线代表方向没有位置概念因此平移不应影响法线。在齐次坐标中我们将法线的www分量设为0.0即可过滤平移。均匀旋转Rotation当物体整体旋转时表面和法线会同步旋转相同的角度。这种情况下直接用模型矩阵的旋转部分左上角3×33\times33×3乘以法线是完全正确的。2. 核心问题非均匀缩放Non-uniform Scaling当物体在不同轴向上的缩放比例不一致时直接使用模型矩阵变换法线就会导致法线偏离垂直方向。【 非均匀缩放对法线的影响示意图 】 原始三角形 (等腰直角) 在 X 轴拉伸 2 倍后 Y Y ▲ ▲ │ │ C ┼ (0,1) C ┼ (0,1) │ \ ◄── 法线 N (0.7, 0.7) │ \ ◄── 错误法线 N (1.4, 0.7) 不再垂直 │ \ │ \ └────┴─────► X └─────┴──────────► X A(0,0) B(1,0) A(0,0) B(2,0) ▲▲▲ 正确法线应该更倾向于 Y 轴如上图所示原始斜边的切线方向为(1,−1)(1, -1)(1,−1)法线为(1,1)(1, 1)(1,1)点积为000正交。如果在 X 轴缩放 2 倍斜边上的点B(1,0)B(1,0)B(1,0)变为B′(2,0)B(2,0)B′(2,0)新斜边切线方向变为(2,−1)(2, -1)(2,−1)。若直接对原法线(1,1)(1, 1)(1,1)乘以相同的缩放矩阵X 轴乘 2得到的新法线为(2,1)(2, 1)(2,1)。验证正交性新切线与新法线的点积为2×2(−1)×13≠02 \times 2 (-1) \times 1 3 \ne 02×2(−1)×130。法线不再垂直于表面为了修正这种由非均匀缩放导致的畸变必须推导出一个能让变换后的法线依然垂直于新切线的矩阵。二、 严密的数学推导我们采用线性代数中的标准列向量记法进行推导。设在物体表面某点处TTT为该点处的一个切线向量Tangent Vector。NNN为该点处的法线向量Normal Vector。由于法线垂直于表面切线它们的点积为零。写成矩阵乘法形式列向量的内积为TTNT^T NTTNTTN0T^T N 0TTN0现在我们对物体施加模型变换矩阵MMM在此指代去除平移后的左上角3×33\times33×3矩阵切线的变换切线是由表面上两个无限接近的顶点相减得到的方向向量。因为顶点是由MMM变换的P′MPP M PP′MP所以切线也必须直接使用MMM进行变换T′MTT M TT′MT法线的变换假设我们寻找的法线变换矩阵为GGG则变换后的法线为N′GNN G NN′GN为了保证变换后的法线N′NN′依然与新切线T′TT′垂直必须满足(T′)TN′0(T)^T N 0(T′)TN′0将T′MTT M TT′MT和N′GNN G NN′GN代入上式(MT)T(GN)0(M T)^T (G N) 0(MT)T(GN)0根据矩阵转置的性质(AB)TBTAT(A B)^T B^T A^T(AB)TBTAT展开左侧乘积TTMTGN0T^T M^T G N 0TTMTGN0对比初始条件TTN0T^T N 0TTN0。要使上式对任意切线TTT和法线NNN都恒成立中间的矩阵乘积MTGM^T GMTG必须为单位矩阵IIIMTGIM^T G IMTGI解方程求出法线变换矩阵GGGG(MT)−1(M−1)TG (M^T)^{-1} (M^{-1})^TG(MT)−1(M−1)T结论用于变换法线的矩阵GGG必须是模型矩阵MMM的逆矩阵的转置。这就是“逆矩阵的转置”的数学来源。三、 几何直观理解为什么“逆矩阵的转置”能纠正缩放导致的倾斜逆矩阵Inverse的作用逆矩阵代表的是相反的变换。如果我们在 X 轴方向将物体拉伸了 2 倍缩放因子为 2那么在逆矩阵中X 轴方向的缩放因子就变成了12\frac{1}{2}21。这非常符合几何直观当表面在 X 方向被拉宽、坡度变缓时法线为了保持垂直应该向 Y 轴方向倾斜也就是在 X 方向收缩。转置矩阵Transpose的作用转置操作保证了向量在正交投影下的乘法方向正确将“行向量式”的受力分布重新映射回列向量。四、 两种特殊情况下的简化由于求逆矩阵Inverse在计算机中是比较昂贵的操作在实际开发中我们不需要对所有物体都计算逆转置。情况 1纯旋转和平移无缩放如果模型矩阵只包含旋转和平移那么其左上角的3×33\times33×3矩阵MMM是一个正交矩阵Orthogonal Matrix。根据正交矩阵的定义其转置矩阵等于逆矩阵MTM−1⇒(M−1)T(MT)TMM^T M^{-1} \Rightarrow (M^{-1})^T (M^T)^T MMTM−1⇒(M−1)T(MT)TM优化结论当物体没有缩放时法线变换矩阵就是MMM本身。我们可以直接使用模型矩阵的3×33\times33×3部分乘以法线。情况 2包含均匀缩放Uniform Scaling如果物体在 X、Y、Z 轴上的缩放因子相同设为sss则MsRM s RMsRRRR为旋转矩阵。它的逆转置矩阵为(M−1)T((sR)−1)T(1sR−1)T1s(R−1)T1sR(M^{-1})^T \left((s R)^{-1}\right)^T \left(\frac{1}{s} R^{-1}\right)^T \frac{1}{s} (R^{-1})^T \frac{1}{s} R(M−1)T((sR)−1)T(s1R−1)Ts1(R−1)Ts1R优化结论均匀缩放只会改变变换后法线向量的长度乘以系数1s\frac{1}{s}s1而不会改变其方向。因此我们依然可以直接用MMM的3×33\times33×3部分来变换法线只需在变换后将法线重新**归一化Normalize**即可。五、 GLSL 代码应用与性能优化在编写 Shader 时计算法线矩阵有两种选择。方案 A在顶点着色器VS中动态计算不推荐#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 为例#includeglm/glm.hpp#includeglm/gtc/matrix_transform.hppglm::mat4 modelMatrixgetModelMatrix();// 获取当前物体的 model 矩阵// 1. 提取左上角 3x3 矩阵求逆并求转置glm::mat3 normalMatrixglm::transpose(glm::inverse(glm::mat3(modelMatrix)));// 2. 传给 Shadershader.setMat3(normalMatrix,normalMatrix);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); }六、 总结问题物理/数学本质解决方案为什么普通变换矩阵不行非均匀缩放会破坏切线与法线的垂直夹角。必须引入与原缩放相反方向的变换。为什么是“逆矩阵的转置”基于(MT)T(GN)0(M T)^T (G N) 0(MT)T(GN)0推导出MTGIM^T G IMTGI从而求得G(M−1)TG (M^{-1})^TG(M−1)T。完美保持正交性。如何优化性能逆矩阵计算量大避免在 GPU 逐顶点计算。在 CPU 算好后以uniform mat3形式送入 Shader。