You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

逐像素光照着色器渲染球体出现类逐顶点光照效果问题

问题描述

我实现了一个简单的逐像素光照(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,最后直接传递给片元着色器使用。但顶点间的法线插值后,向量长度会发生变化,不再是单位向量,这会导致片元阶段的点积计算错误,最终出现类似逐顶点光照的效果。

修复步骤:

  1. 调整顶点着色器的法线处理:去掉顶点阶段对输入法线的归一化,仅做矩阵变换:
NormalEyePosition = NormalMatrix * Normal;
  1. 在片元着色器中重新归一化法线:插值后的法线需要重新归一化,确保是单位向量:
vec3 N = normalize(NormalEyePosition);

额外检查项:

  • 球体法线生成是正确的:顶点位置归一化后就是球体法线,这部分代码没问题。
  • 确认NormalMatrix的计算正确性:它应该是ModelViewMatrix的逆矩阵的转置,尤其是当模型有非均匀缩放时,必须保证这个矩阵正确,否则法线方向会被拉伸扭曲。
  • 顶点属性绑定确认:确保法线属性正确传入着色器,没有和其他属性(如位置、UV)混绑。

为什么立方体/平面表现正常?

这类模型的同一三角形内法线方向一致,插值后法线长度不会发生明显变化,所以归一化时机的错误影响不大;但球体每个顶点的法线方向都不同,插值后的法线长度偏离1的问题会被放大,直接导致光照计算错误。

内容的提问来源于stack exchange,提问作者Mario

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 13:51:58