逐像素光照着色器渲染球体出现类逐顶点光照效果问题
问题描述
我实现了一个简单的逐像素光照(per-pixel-lighting)着色器,但渲染出的球体呈现出类似逐顶点光照(per-vertex shading)的效果。已经排查一天了,没找到问题所在。球体的法线是否与归一化后的顶点位置一致?
该着色器在渲染立方体或平面时表现正常。
顶点着色器
#version 330 core uniform mat4 ModelViewProjectionMatrix; uniform mat4 ModelViewMatrix; uniform mat3 NormalMatrix; in vec3 Position; in vec3 Normal; out vec3 VertexEyePosition; out vec3 NormalEyePosition; void main() { gl_Position = ModelViewProjectionMatrix * vec4(Position, 1.0f); VertexEyePosition = vec3(ModelViewMatrix * vec4(Position, 1.0f)); NormalEyePosition = NormalMatrix * normalize(Normal); }
片元着色器
#version 330 core uniform vec4 EyeLightPosition; in vec3 VertexEyePosition; in vec3 NormalEyePosition; out vec4 FinalColor; void main() { vec3 L = normalize(vec3(EyeLightPosition) - VertexEyePosition); vec3 N = NormalEyePosition; vec3 R = reflect(-L, N); vec3 V = normalize(-VertexEyePosition); vec3 Ambient = 0.1 * vec3(1.0, 0.0, 0.0); vec3 Diffuse = 0.8 * vec3(1.0, 0.0, 0.0) * max(0.0, dot(N, L)); vec3 Specular = vec3(1.0) * pow(max(0.0, dot(R, V)), 8.0); FinalColor = vec4(Ambient + Diffuse + Specular, 1.0); }
球体生成代码
void SphereMesh::GenerateGeometry() { vertices.clear(); uv_coords.clear(); normals.clear(); faces.clear(); vertices.reserve((segments + 1) * (rings + 1)); uv_coords.reserve((segments + 1) * (rings + 1)); // Generate vertices and UV coordinates; for (unsigned int ring = 0; ring <= rings; ring++) { const float ring_sin = glm::sin(static_cast<float>(ring) / rings * glm::pi<float>() - 0.5f * glm::pi<float>()); const float ring_cos = glm::cos(static_cast<float>(ring) / rings * glm::pi<float>() - 0.5f * glm::pi<float>()); for (unsigned int segment = 0; segment <= segments; segment++) { if (((ring == 0) || (ring == rings)) && (segment == segments)) continue; const float segment_sin = glm::sin(static_cast<float>(segment) / segments * 2.0f * glm::pi<float>()); const float segment_cos = glm::cos(static_cast<float>(segment) / segments * 2.0f * glm::pi<float>()); const float x = radius * ring_cos * segment_sin; const float y = radius * ring_sin; const float z = radius * ring_cos * segment_cos; const float u = ((ring == 0) || (ring == rings)) && (segment == segments) ? static_cast<float>(segment) / (segments - 1) + 0.5f * static_cast<float>(segment) / (segments - 1): static_cast<float>(segment) / segments; const float v = static_cast<float>(ring) / rings; vertices.emplace_back(x, y, z); uv_coords.emplace_back(u, v); normals.emplace_back(glm::normalize(glm::vec3(x, y, z))); } } // Generate faces for (unsigned int ring = 0; ring < rings; ring++) { for (unsigned int segment = 0; segment < segments; segment++) { if (ring == 0) // Lower cap { Face face; face.a = segment; face.b = segments + segment + 1; face.c = segments + segment; faces.push_back(face); } else if (ring < rings - 1) // Body { const unsigned int v1 = segments + ((ring - 1) * (segments + 1)) + segment; const unsigned int v2 = segments + ((ring - 1) * (segments + 1)) + segment + 1; const unsigned int v3 = segments + ((ring - 1 + 1) * (segments + 1)) + segment; const unsigned int v4 = segments + ((ring - 1 + 1) * (segments + 1)) + segment + 1; Face face1, face2; face1.a = v1; face1.b = v2; face1.c = v3; face2.a = v3; face2.b = v2; face2.c = v4; faces.push_back(face1); faces.push_back(face2); } else // Upper cap { Face face; face.a = segments + ((ring - 1) * (segments + 1)) + segment; face.b = segments + ((ring - 1) * (segments + 1)) + segment + 1; face.c = segments + ((ring - 1 + 1) * (segments + 1)) + segment; faces.push_back(face); } } } }
渲染结果

问题排查与解决
核心问题:法线归一化时机错误
你在顶点着色器中对输入法线先做了归一化,再乘以NormalMatrix,最后直接传递给片元着色器使用。但顶点间的法线插值后,向量长度会发生变化,不再是单位向量,这会导致片元阶段的点积计算错误,最终出现类似逐顶点光照的效果。
修复步骤:
- 调整顶点着色器的法线处理:去掉顶点阶段对输入法线的归一化,仅做矩阵变换:
NormalEyePosition = NormalMatrix * Normal;
- 在片元着色器中重新归一化法线:插值后的法线需要重新归一化,确保是单位向量:
vec3 N = normalize(NormalEyePosition);
额外检查项:
- 球体法线生成是正确的:顶点位置归一化后就是球体法线,这部分代码没问题。
- 确认
NormalMatrix的计算正确性:它应该是ModelViewMatrix的逆矩阵的转置,尤其是当模型有非均匀缩放时,必须保证这个矩阵正确,否则法线方向会被拉伸扭曲。 - 顶点属性绑定确认:确保法线属性正确传入着色器,没有和其他属性(如位置、UV)混绑。
为什么立方体/平面表现正常?
这类模型的同一三角形内法线方向一致,插值后法线长度不会发生明显变化,所以归一化时机的错误影响不大;但球体每个顶点的法线方向都不同,插值后的法线长度偏离1的问题会被放大,直接导致光照计算错误。
内容的提问来源于stack exchange,提问作者Mario
相关产品推荐
相关产品推荐

