Love2D 3D渲染引擎法线贴图光照异常问题排查
Love2D 3D渲染引擎法线贴图异常修复方案
核心错误排查
- 视线方向计算反向:观察空间下相机默认位于坐标原点,视线方向为片元指向相机,原有代码计算为相机指向片元,方向完全颠倒,直接导致光照沿相机位置分割、高光计算错误。
- 法线/切线变换矩阵使用错误:法线、切线属于方向向量,不能直接使用modelView矩阵变换,原有代码未使用专门的法线矩阵(modelView矩阵的逆转置3x3矩阵),导致模型存在缩放时法线方向扭曲,这也是归一化顶点法线后出现黑屏的核心原因。
- TBN矩阵未做正交化处理:按三角形面平均计算得到的顶点切线,无法保证与顶点法线严格垂直,直接叉乘得到的副切线会导致TBN三个基向量不正交,变换切线空间法线时出现方向偏移,触发三角形面之间光照交替跳变。
- 反射计算入射方向不符合接口要求:GLSL内置
reflect函数要求入射光线向量指向表面,原有代码传入的是从片元指向光源的向量,方向反向,导致高光计算完全错误。
修复后代码
顶点着色器
// 顶点属性输入 attribute vec3 VertexPosition; attribute vec3 VertexNormal; attribute vec3 VertexTangent; attribute vec2 TexCoord; // Uniform矩阵 uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; uniform mat3 normalMatrix; // 预计算值:transpose(inverse(mat3(viewMatrix * modelMatrix))) // 传递给片元着色器的 varying 变量 varying vec3 fragmentPosition_viewSpace; varying vec2 texture_coords; varying mat3 tbnMatrix_viewSpace; void main() { texture_coords = TexCoord; // 计算观察空间片元位置 vec4 viewSpacePos = viewMatrix * modelMatrix * vec4(VertexPosition, 1.0); fragmentPosition_viewSpace = viewSpacePos.xyz; gl_Position = projectionMatrix * viewSpacePos; // 用法线矩阵正确变换T、N向量到观察空间 vec3 T = normalize(normalMatrix * VertexTangent); vec3 N = normalize(normalMatrix * VertexNormal); // Gram-Schmidt 重正交,保证T与N严格垂直,消除平均切线带来的正交误差 T = normalize(T - dot(T, N) * N); // 按右手坐标系叉乘计算副切线B vec3 B = cross(N, T); // 构造观察空间TBN矩阵 tbnMatrix_viewSpace = mat3(T, B, N); }
片元着色器
varying vec3 fragmentPosition_viewSpace; varying vec2 texture_coords; varying mat3 tbnMatrix_viewSpace; // 纹理采样器 uniform sampler2D diffuseTextureSampler; uniform sampler2D normalTextureSampler; uniform sampler2D specularTextureSampler; // 光照参数 uniform vec3 lightPosition_worldSpace; uniform vec3 lightColor; uniform float lightIntensity; uniform float specularLobeFactor; uniform mat4 viewMatrix; // 对齐原有项目的纹理采样封装 vec4 Texel(sampler2D samp, vec2 uv) { return texture2D(samp, uv); } void main() { vec4 diffuseColor = Texel(diffuseTextureSampler, texture_coords); vec4 normalColor = Texel(normalTextureSampler, texture_coords); vec4 specularColor = Texel(specularTextureSampler, texture_coords); // 采样切线空间法线,映射到[-1, 1]区间后通过TBN矩阵变换到观察空间 vec3 tangentNormal = normalize(normalColor.rgb * 2.0 - 1.0); // 若使用DX格式法线贴图(Y轴朝下),取消下一行注释 // tangentNormal.y = -tangentNormal.y; vec3 normal_viewSpace = normalize(tbnMatrix_viewSpace * tangentNormal); // 计算观察空间光照方向 vec3 lightPosition_viewSpace = (viewMatrix * vec4(lightPosition_worldSpace, 1.0)).xyz; vec3 lightDirection_viewSpace = normalize(lightPosition_viewSpace - fragmentPosition_viewSpace); // 修复视线方向:从片元指向原点(相机位置) vec3 viewDirection_viewSpace = normalize(-fragmentPosition_viewSpace); // 光照衰减计算 vec3 lightColorIntensity = lightColor * lightIntensity; float distanceFromLight = length(lightPosition_viewSpace - fragmentPosition_viewSpace); float attenuation = 1.0 / (distanceFromLight * distanceFromLight); // 漫反射光照 float diffuseStrength = max(dot(normal_viewSpace, lightDirection_viewSpace), 0.0); vec3 diffuseLight = lightColorIntensity * diffuseStrength * attenuation; // 修复高光反射计算:入射光线方向指向表面,传入-lightDirection_viewSpace vec3 reflectDirection = reflect(-lightDirection_viewSpace, normal_viewSpace); float specularStrength = pow(max(dot(viewDirection_viewSpace, reflectDirection), 0.0), specularLobeFactor); vec3 specularLight = lightColorIntensity * specularStrength * attenuation; // 合并输出颜色 gl_FragColor.rgb = diffuseColor.rgb * diffuseLight + specularColor.rgb * specularLight; gl_FragColor.a = diffuseColor.a; }
额外注意事项
- 顶点切线计算阶段,需确保每个三角形的切线方向与UV坐标的U轴严格对齐,共享顶点的切线需要按三角形面积加权平均,避免相邻三角形切线方向差异过大导致的光照硬边。
- 若项目中所有模型都只做均匀缩放,可直接使用
mat3(viewMatrix * modelMatrix)代替预计算的normalMatrix,但使用逆转置法线矩阵是通用兼容方案,不会出现法线方向错误。 - 若修复后仍出现法线方向上下颠倒的问题,检查使用的法线贴图格式,OpenGL格式法线Y轴朝上,DX格式法线Y轴朝下,按代码中注释调整Y分量符号即可。
内容的提问来源于stack exchange,提问作者Razorboot
相关产品推荐
相关产品推荐

