【qt openGL入门笔记】基础光照

前言

接续上一篇的qt项目,增加光源效果的功能。本篇增加了以下知识点:

  • 显示两个物体,两个VBO
  • 数组型GLfloat和容器型GLfloat
  • 增加法线
  • 增加光照的三个分量:ambient(环境光)+ diffuse(漫反射)+ specular(镜面高光)
  • 两个物体,不同的顶点着色器、片段着色器、MVP
  • keyPressEvent增加光源左右移动事件

效果图

黄色圆球块是光源位置,可以看到光源在哪个位置,正中心的物体朝向光源的位面是亮的。

新增知识点

1.两个VBO

为了让光源可视化,增加一个圆球显示在光源的位置(它不是光源的实例,只是显示一个物体并移动到光源的位置而已),因为形状坐标不一样,无法共用一个VBO,那就创建两个VBO。

复制代码
QOpenGLBuffer m_vbo{QOpenGLBuffer::VertexBuffer};      // 立方体:位置 + 法线
QOpenGLBuffer m_lampVbo{QOpenGLBuffer::VertexBuffer};  // ★ 光源球:只有位置,独立一份 VBO

2.数组型GLfloat和容器型GLfloat

写代码时就能数清有几个顶点 → 用数组;顶点靠循环算法算出来、数量随时会变 → 用 vector(循环生成的如果顶点足够多也可以用数组)。

OpenGL 最终要的,就是一大坨连续排在一起的浮点数(顶点坐标)。无论是数组还是 vector,最后喂给 GPU 的内容完全一样。

GLfloat vertices[](数组) std::vector<GLfloat>(vector)
大小 写死,编译时定好,不能变 可以随时增长、缩小
怎么填内容 一次性 {...} 写全,或手动下标 push_back 一个个加
知道有几个元素 用 sizeof 间接算 直接 .size()
内存 在栈上,容量大了有溢出风险 在堆上,自己管理
适合 顶点固定、数量已知(立方体、三角形) 顶点靠循环生成、数量运行时才定(球、圆、模型)

2.1六面物体的GLfloat

复制代码
// ============================================================
    // 1. 立方体顶点数据:36 个顶点 = 6 面 × 2 三角形 × 3 顶点
    //    法线 = 垂直于该面的单位向量,告诉着色器"这个面朝哪"
    //    同一面上的 6 个顶点法线相同(平面),curved 模型则逐顶点不同
    // ============================================================
    GLfloat vertices[] = {
        // ---- 位置 x,y,z ----      ---- 法线 nx,ny,nz ----
        // 前面 (z = +0.5),法线朝 +Z
        -0.5f,-0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
         0.5f,-0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
         0.5f, 0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
        -0.5f,-0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
         0.5f, 0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
        -0.5f, 0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
        // 后面 (z = -0.5),法线朝 -Z
        -0.5f,-0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
         0.5f,-0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
         0.5f, 0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
        -0.5f,-0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
         0.5f, 0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
        -0.5f, 0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
        // 上面 (y = +0.5),法线朝 +Y
        -0.5f, 0.5f,-0.5f,   0.0f, 1.0f, 0.0f,
         0.5f, 0.5f,-0.5f,   0.0f, 1.0f, 0.0f,
         0.5f, 0.5f, 0.5f,   0.0f, 1.0f, 0.0f,
        -0.5f, 0.5f,-0.5f,   0.0f, 1.0f, 0.0f,
         0.5f, 0.5f, 0.5f,   0.0f, 1.0f, 0.0f,
        -0.5f, 0.5f, 0.5f,   0.0f, 1.0f, 0.0f,
        // 下面 (y = -0.5),法线朝 -Y
        -0.5f,-0.5f,-0.5f,   0.0f,-1.0f, 0.0f,
         0.5f,-0.5f,-0.5f,   0.0f,-1.0f, 0.0f,
         0.5f,-0.5f, 0.5f,   0.0f,-1.0f, 0.0f,
        -0.5f,-0.5f,-0.5f,   0.0f,-1.0f, 0.0f,
         0.5f,-0.5f, 0.5f,   0.0f,-1.0f, 0.0f,
        -0.5f,-0.5f, 0.5f,   0.0f,-1.0f, 0.0f,
        // 右面 (x = +0.5),法线朝 +X
         0.5f,-0.5f, 0.5f,   1.0f, 0.0f, 0.0f,
         0.5f,-0.5f,-0.5f,   1.0f, 0.0f, 0.0f,
         0.5f, 0.5f,-0.5f,   1.0f, 0.0f, 0.0f,
         0.5f,-0.5f, 0.5f,   1.0f, 0.0f, 0.0f,
         0.5f, 0.5f,-0.5f,   1.0f, 0.0f, 0.0f,
         0.5f, 0.5f, 0.5f,   1.0f, 0.0f, 0.0f,
        // 左面 (x = -0.5),法线朝 -X
        -0.5f,-0.5f, 0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f,-0.5f,-0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f,-0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f,-0.5f, 0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f,-0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f, 0.5f,  -1.0f, 0.0f, 0.0f,
    };

    //VBO:把数据从内存搬进显存
    m_vbo.create();
    m_vbo.bind();
    m_vbo.allocate(vertices, sizeof(vertices));

2.2圆形的GLfloat

复制代码
// ============================================================
    // 1b. ★ 光源球顶点数据(独立 VBO)
    //     用球面坐标(经纬网格)生成一个单位球:radius = 1.0
    //     stacks = 纬线圈数,sectors = 经线分块数,越大越圆滑
    //     光源是"自发光"的,不需要法线,所以每个顶点只存 3 个 float 位置
    //     这里直接展开成三角形列表(不用索引缓冲),和立方体的 glDrawArrays 风格一致
    // ============================================================
    {
        std::vector<GLfloat> sphere;

        const int   stacks  = 36;     // 沿南北方向切成 36 层
        const int   sectors = 36;     // 沿东西方向切成 36 份
        const float radius  = 1.0f;   // 单位球,实际大小靠 lampModel.scale 控制
        const float PI      = 3.14159265358979f;
        for (int i = 0; i < stacks; ++i) {
            // 纬度:从 -PI/2(南极)扫到 +PI/2(北极)
            float lat0 = PI * (-0.5f + (float)i        / stacks);
            float lat1 = PI * (-0.5f + (float)(i + 1)  / stacks);
            for (int j = 0; j < sectors; ++j) {
                // 经度:绕一圈 0 ~ 2*PI
                float lon0 = 2.0f * PI * (float)j       / sectors;
                float lon1 = 2.0f * PI * (float)(j + 1) / sectors;

                auto P = [&](float lat, float lon) {
                    return QVector3D(radius * std::cos(lat) * std::cos(lon),
                                     radius * std::sin(lat),
                                     radius * std::cos(lat) * std::sin(lon));
                };

                QVector3D v00 = P(lat0, lon0);
                QVector3D v01 = P(lat0, lon1);
                QVector3D v11 = P(lat1, lon1);
                QVector3D v10 = P(lat1, lon0);

                // 一个网格 = 两个三角形:v00-v10-v11 和 v00-v11-v01
                QVector3D tri[6] = { v00, v10, v11,  v00, v11, v01 };
                for (const QVector3D& v : tri) {
                    sphere.push_back(v.x());
                    sphere.push_back(v.y());
                    sphere.push_back(v.z());
                }
            }
        }

        m_lampVertexCount = (int)sphere.size() / 3;

        m_lampVbo.create();
        m_lampVbo.bind();
        m_lampVbo.allocate(sphere.data(), sphere.size() * sizeof(GLfloat));
    }

3.增加法线

3.1 GLfloat增项

把上一篇物体4、5、6列存储颜色的值改为存储法线

复制代码
        // - 位置 x,y,z --   -- 法线 nx,ny,nz -
        // 前面 (z = +0.5),法线朝 +Z
        -0.5f,-0.5f, 0.5f,   0.0f, 0.0f, 1.0f,

3.2重构mvp模型增项

复制代码
// ★ 光照新增:法线矩阵(用于把法线从"模型空间"转到"世界空间")
    QMatrix3x3 m_normalMatrix;

void GLWidget::buildMatrices(int w, int h)
{
    ... ...

    // ---- 4. ★ 光照新增:法线矩阵 ----
    // Qt 的 QMatrix4x4 自带 normalMatrix() 方法,直接返回
    // "模型矩阵的逆转置矩阵的 3x3 子矩阵"------正是法线需要的变换
    // (底层逻辑见下文说明,不用 Qt 这个方法也可以自己算)
    m_normalMatrix = m_model.normalMatrix();
}

4.物体着色器

4.1顶点着色器增项

增加输入变量aNormal,输出变量Normal,uniform参数uNormalMatrix

复制代码
// ============================================================
    // 2. 立方体的顶点着色器:多传两个 out
    //    - FragPos:顶点变换到"世界空间"后的坐标(算光照方向要用)
    //    - Normal :法线经法线矩阵变换后的方向(判断面朝向)
    // ============================================================
    m_program.addShaderFromSourceCode(QOpenGLShader::Vertex,
        "#version 150\n"
        "in vec3 aPos;\n"                               // 顶点位置(局部空间)
        "in vec3 aNormal;\n"                            // ★ 新增:法线(局部空间)
        "out vec3 FragPos;\n"                           // 传给片段着色器:世界空间坐标
        "out vec3 Normal;\n"                            // 传给片段着色器:世界空间法线
        "uniform mat4 uModel;\n"
        "uniform mat4 uView;\n"
        "uniform mat4 uProjection;\n"
        "uniform mat3 uNormalMatrix;\n"                 // ★ 新增:法线矩阵(3x3,无平移分量)
        "void main() {\n"
        // 把顶点从局部空间 → 世界空间(供片段着色器计算"顶点→光源"方向)
        "    FragPos = vec3(uModel * vec4(aPos, 1.0));\n"
        // ★ 法线必须用法线矩阵变换,不能用 uModel 直接乘(原因见下文说明)
        "    Normal = uNormalMatrix * aNormal;\n"
        "    gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);\n"
        "}"
    );

4.2片段着色器增项

增加输入的法线变量Normal,光源uniform参数uLightPos、uViewPos、uLightColor、uObjectColor

复制代码
// ============================================================
    // 3. 立方体的片段着色器:Phong 光照的核心计算都在这
    //    三个分量:ambient(环境光)+ diffuse(漫反射)+ specular(镜面高光)
    // ============================================================
    m_program.addShaderFromSourceCode(QOpenGLShader::Fragment,
        "#version 150\n"
        "in vec3 FragPos;\n"          // 接收自顶点着色器:世界空间坐标(GPU 自动插值)
        "in vec3 Normal;\n"           // 接收自顶点着色器:世界空间法线(GPU 自动插值)
        "out vec4 FragColor;\n"
        // ---- 光源参数 uniform(由 C++ 侧 setUniformValue 传入)----
        "uniform vec3 uLightPos;\n"       // 光源位置(世界空间)
        "uniform vec3 uViewPos;\n"        // 相机位置(世界空间,算高光方向要用)
        "uniform vec3 uLightColor;\n"     // 光的颜色
        "uniform vec3 uObjectColor;\n"    // 物体基色
        "void main() {\n"
        // ========== 分量 1:环境光 Ambient ==========
        // 模拟"到处反弹的杂散光",让背光面不会纯黑
        // 强度系数 0.1 表示环境光很弱,只提供 10% 基础亮度
        "    float ambientStrength = 0.1;\n"
        "    vec3 ambient = ambientStrength * uLightColor;\n"
        // ========== 分量 2:漫反射 Diffuse ==========
        // 物体"正对光源"的部分最亮,背对的部分最暗
        "    vec3 norm = normalize(Normal);\n"  // 法线归一化(插值后长度可能不再是 1)
        // 光线方向 = 顶点位置 → 光源位置 的单位向量
        "    vec3 lightDir = normalize(uLightPos - FragPos);\n"
        // ★ 点乘的几何意义:两个单位向量的夹角余弦
        //   夹角 0° → dot=1(正对光源,最亮)
        //   夹角 90° → dot=0(擦边照到)
        //   夹角 >90° → dot<0(背光,max 钳位到 0)
        "    float diff = max(dot(norm, lightDir), 0.0);\n"
        "    vec3 diffuse = diff * uLightColor;\n"
        // ========== 分量 3:镜面高光 Specular ==========
        // 模拟"光斑":视线方向越接近"反射光方向",高光越强
        "    float specularStrength = 0.5;\n"   // 高光强度(塑料 0.5,金属更高)
        "    float shininess = 32.0;\n"         // 反光度(越大高光越小越锐利)
        // 视线方向 = 顶点位置 → 相机位置 的单位向量
        "    vec3 viewDir = normalize(uViewPos - FragPos);\n"
        // reflect(I, N):计算向量 I 关于法线 N 的反射向量
        // 注意传 -lightDir(reflect 要求 I 指向入射点)
        "    vec3 reflectDir = reflect(-lightDir, norm);\n"
        // 用 dot(viewDir, reflectDir)^shininess 制造"锐利衰减"的高光斑
        "    float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);\n"
        "    vec3 specular = specularStrength * spec * uLightColor;\n"
        // ========== 合成:三个分量相加,再乘以物体基色 ==========
        "    vec3 result = (ambient + diffuse + specular) * uObjectColor;\n"
        "    FragColor = vec4(result, 1.0);\n"
        "}"
    );

5.光源着色器

它不是光源的实例,只是显示一个物体并移动到光源的位置而已,着色器也是比较简单。

5.1顶点着色器

只有基础的坐标和MVP。

复制代码
 m_lampProgram.addShaderFromSourceCode(QOpenGLShader::Vertex,
        "#version 150\n"
        "in vec3 aPos;\n"
        "uniform mat4 uModel;\n"
        "uniform mat4 uView;\n"
        "uniform mat4 uProjection;\n"
        "void main() {\n"
        "    gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);\n"
        "}"
    );

5.2片段着色器

输出为固定的黄色。

复制代码
m_lampProgram.addShaderFromSourceCode(QOpenGLShader::Fragment,
        "#version 150\n"
        "out vec4 FragColor;\n"
        "void main() {\n"
        "    FragColor = vec4(1.0, 1.0, 0.0, 1.0);\n"   // 纯黄色,代表光源
        "}"
    );

6.paintGL渲染

拆分成2次物体绘制,六面体物体只进行旋转操作m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f);

圆形物体有2个动作:

  1. 平移到光源位置:lampModel.translate(m_lightPos);

  2. 缩小:lampModel.scale(0.2f);

    void GLWidget::paintGL()
    {
    glClearColor(0.05f, 0.05f, 0.12f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    复制代码
     // ============================================================
     // Pass 1:绘制被光照的立方体
     // ============================================================
     m_vbo.bind();   // 立方体自己的 VBO(位置 + 法线)
     m_program.bind();
    
     // 顶点属性布局:每个顶点 6 个 float,前 3 个是位置,后 3 个是法线
     int posAttr    = m_program.attributeLocation("aPos");
     int normalAttr = m_program.attributeLocation("aNormal");
    
     m_program.setAttributeBuffer(posAttr,    GL_FLOAT, 0,                  3, GLF_SIZE * sizeof(GLfloat));
     m_program.enableAttributeArray(posAttr);
     m_program.setAttributeBuffer(normalAttr, GL_FLOAT, 3 * sizeof(GLfloat), 3, GLF_SIZE * sizeof(GLfloat));
     m_program.enableAttributeArray(normalAttr);
    
     // 每帧更新自转---物体只旋转
     m_model.setToIdentity();
     m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f);
     m_normalMatrix = m_model.normalMatrix();   // 法线矩阵也要跟着更新!
    
     // MVP + 法线矩阵
     m_program.setUniformValue("uModel",       m_model);
     m_program.setUniformValue("uView",        m_view);
     m_program.setUniformValue("uProjection",  m_projection);
     m_program.setUniformValue("uNormalMatrix", m_normalMatrix);   // ★ 3x3 矩阵
    
     // ★ 光照专属 uniform:光源位置、相机位置、两种颜色
     //   uViewPos 就是相机位置------因为"高光"需要知道观察者从哪看
     m_program.setUniformValue("uLightPos",    m_lightPos);
     m_program.setUniformValue("uViewPos",     m_cameraPos);
     m_program.setUniformValue("uLightColor",  m_lightColor);
     m_program.setUniformValue("uObjectColor", m_objectColor);
    
     glDrawArrays(GL_TRIANGLES, 0, 36);
    
     m_program.disableAttributeArray(posAttr);
     m_program.disableAttributeArray(normalAttr);
     m_program.release();
    
     // ============================================================
     // Pass 2:绘制光源指示球(独立球体 VBO + 自发光着色器)
     // ============================================================
     m_lampVbo.bind();   // ★ 换成球体专用的 VBO
    
     m_lampProgram.bind();
    
     // 球体顶点只有位置:stride = 3 * sizeof(GLfloat)
     int lampPosAttr = m_lampProgram.attributeLocation("aPos");
     m_lampProgram.setAttributeBuffer(lampPosAttr, GL_FLOAT, 0, 3, 3 * sizeof(GLfloat));
     m_lampProgram.enableAttributeArray(lampPosAttr);
    
     // 光源球的模型矩阵:先缩小成小圆球,再平移到光源位置
     // 注意顺序:先 scale 后 translate(矩阵乘法从右往左作用)
     QMatrix4x4 lampModel;
     lampModel.setToIdentity();
     lampModel.translate(m_lightPos);   // 平移到光源位置
     lampModel.scale(0.2f);             // 单位球缩成半径 0.2 的小圆球
    
     m_lampProgram.setUniformValue("uModel",      lampModel);
     m_lampProgram.setUniformValue("uView",       m_view);
     m_lampProgram.setUniformValue("uProjection", m_projection);
    
     glDrawArrays(GL_TRIANGLES, 0, m_lampVertexCount);   // 画一个圆球
    
     m_lampProgram.disableAttributeArray(lampPosAttr);
     m_lampProgram.release();

    }

这里需要特别注意的是,根据openGL状态机的机制,每次绘制前需要推送指定的配置资源到GPU:

1、绘制被光照的立方体时,需要先绑定VBO和着色器到GPU;

复制代码
// ============================================================
    // Pass 1:绘制被光照的立方体
    // ============================================================
    m_vbo.bind();   // 立方体自己的 VBO(位置 + 法线)
    m_program.bind();

2、绘制光源指示球时,需要先绑定VBO和着色器到GPU;

复制代码
 // ============================================================
    // Pass 2:绘制光源指示球(独立球体 VBO + 自发光着色器)
    // ============================================================
    m_lampVbo.bind();   // ★ 换成球体专用的 VBO
    m_lampProgram.bind();

7.键盘事件

增加移动事件,通过修改m_lightPos值,实现圆形物体的平移lampModel.translate(m_lightPos);

复制代码
void GLWidget::keyPressEvent(QKeyEvent *event)
{
    const float speed = 0.3f;
    switch (event->key()) {
    ... ...                                       
    // ★ 加个实验:用 J/K 移动光源的 X 位置,直观感受"光源移动对光照的影响"
    case Qt::Key_J: m_lightPos.setX(m_lightPos.x() - 0.2f); break;
    case Qt::Key_K: m_lightPos.setX(m_lightPos.x() + 0.2f); break;
    }
    buildMatrices(width(), height());
    update();
}

核心代码

glwidget.h

复制代码
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QMatrix4x4>
#include <QVector3D>
#include <QTimer>
#include <QKeyEvent>

//每个顶点的 float 数量(立方体):3 位置 + 3 法线 = 6
#define GLF_SIZE 6

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
    Q_OBJECT
public:
    explicit GLWidget(QWidget *parent = nullptr);
    ~GLWidget();

protected:
    void initializeGL() override;
    void resizeGL(int w, int h) override;
    void paintGL() override;
    void keyPressEvent(QKeyEvent *event) override;   // WASD 移动相机

private:
    QOpenGLShaderProgram m_program;      // 立方体的"被光照"着色器
    QOpenGLShaderProgram m_lampProgram;  // 光源指示球的"自发光"着色器

    QOpenGLBuffer m_vbo{QOpenGLBuffer::VertexBuffer};      // 立方体:位置 + 法线
    QOpenGLBuffer m_lampVbo{QOpenGLBuffer::VertexBuffer};  // ★ 光源球:只有位置,独立一份 VBO
    int m_lampVertexCount = 0;                              // ★ 球体顶点数(供 glDrawArrays 用)

    QTimer *m_timer;

    // 三大矩阵:MVP
    QMatrix4x4 m_model;
    QMatrix4x4 m_view;
    QMatrix4x4 m_projection;

    // ★ 光照新增:法线矩阵(用于把法线从"模型空间"转到"世界空间")
    QMatrix3x3 m_normalMatrix;

    float m_angle = 0.0f;                // 立方体自转角度

    // ---- 相机参数(决定 View 矩阵)----
    QVector3D m_cameraPos    = QVector3D(0.0f, 0.0f, 5.0f);
    QVector3D m_cameraTarget = QVector3D(0.0f, 0.0f, 0.0f);
    QVector3D m_cameraUp     = QVector3D(0.0f, 1.0f, 0.0f);

    // ---- ★ 光照新增:光源参数(决定 Phong 计算的输入)----
    QVector3D m_lightPos    = QVector3D(0.0f, 1.0f, 2.0f);  // 光源在世界空间的位置
    QVector3D m_lightColor  = QVector3D(1.0f, 1.0f, 1.0f);  // 光的颜色(白光)
    QVector3D m_objectColor = QVector3D(1.0f, 0.5f, 0.31f); // 物体基色(珊瑚橙)

    void buildMatrices(int w, int h);
};

#endif // GLWIDGET_H

glwidget.cpp

复制代码
#include "glwidget.h"
#include <QDebug>
#include <cmath>
#include <vector>

GLWidget::GLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{
    setFocusPolicy(Qt::StrongFocus);   // 别忘了:键盘焦点(上次踩过的坑)

    m_timer = new QTimer(this);
    connect(m_timer, &QTimer::timeout, this, [=]{
        m_angle += 1.5f;
        if (m_angle >= 360.0f) m_angle -= 360.0f;
        update();   // 触发 paintGL 重绘
    });
}

GLWidget::~GLWidget() = default;

void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glEnable(GL_DEPTH_TEST);   // 3D 必须开深度测试

    // ============================================================
    // 1. 立方体顶点数据:36 个顶点 = 6 面 × 2 三角形 × 3 顶点
    //    法线 = 垂直于该面的单位向量,告诉着色器"这个面朝哪"
    //    同一面上的 6 个顶点法线相同(平面),curved 模型则逐顶点不同
    // ============================================================
    GLfloat vertices[] = {
        // ---- 位置 x,y,z ----      ---- 法线 nx,ny,nz ----
        // 前面 (z = +0.5),法线朝 +Z
        -0.5f,-0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
         0.5f,-0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
         0.5f, 0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
        -0.5f,-0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
         0.5f, 0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
        -0.5f, 0.5f, 0.5f,   0.0f, 0.0f, 1.0f,
        // 后面 (z = -0.5),法线朝 -Z
        -0.5f,-0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
         0.5f,-0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
         0.5f, 0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
        -0.5f,-0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
         0.5f, 0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
        -0.5f, 0.5f,-0.5f,   0.0f, 0.0f,-1.0f,
        // 上面 (y = +0.5),法线朝 +Y
        -0.5f, 0.5f,-0.5f,   0.0f, 1.0f, 0.0f,
         0.5f, 0.5f,-0.5f,   0.0f, 1.0f, 0.0f,
         0.5f, 0.5f, 0.5f,   0.0f, 1.0f, 0.0f,
        -0.5f, 0.5f,-0.5f,   0.0f, 1.0f, 0.0f,
         0.5f, 0.5f, 0.5f,   0.0f, 1.0f, 0.0f,
        -0.5f, 0.5f, 0.5f,   0.0f, 1.0f, 0.0f,
        // 下面 (y = -0.5),法线朝 -Y
        -0.5f,-0.5f,-0.5f,   0.0f,-1.0f, 0.0f,
         0.5f,-0.5f,-0.5f,   0.0f,-1.0f, 0.0f,
         0.5f,-0.5f, 0.5f,   0.0f,-1.0f, 0.0f,
        -0.5f,-0.5f,-0.5f,   0.0f,-1.0f, 0.0f,
         0.5f,-0.5f, 0.5f,   0.0f,-1.0f, 0.0f,
        -0.5f,-0.5f, 0.5f,   0.0f,-1.0f, 0.0f,
        // 右面 (x = +0.5),法线朝 +X
         0.5f,-0.5f, 0.5f,   1.0f, 0.0f, 0.0f,
         0.5f,-0.5f,-0.5f,   1.0f, 0.0f, 0.0f,
         0.5f, 0.5f,-0.5f,   1.0f, 0.0f, 0.0f,
         0.5f,-0.5f, 0.5f,   1.0f, 0.0f, 0.0f,
         0.5f, 0.5f,-0.5f,   1.0f, 0.0f, 0.0f,
         0.5f, 0.5f, 0.5f,   1.0f, 0.0f, 0.0f,
        // 左面 (x = -0.5),法线朝 -X
        -0.5f,-0.5f, 0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f,-0.5f,-0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f,-0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f,-0.5f, 0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f,-0.5f,  -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f, 0.5f,  -1.0f, 0.0f, 0.0f,
    };
    //VBO:把数据从内存搬进显存
    m_vbo.create();
    m_vbo.bind();
    m_vbo.allocate(vertices, sizeof(vertices));

    // ============================================================
    // 1b. ★ 光源球顶点数据(独立 VBO)
    //     用球面坐标(经纬网格)生成一个单位球:radius = 1.0
    //     stacks = 纬线圈数,sectors = 经线分块数,越大越圆滑
    //     光源是"自发光"的,不需要法线,所以每个顶点只存 3 个 float 位置
    //     这里直接展开成三角形列表(不用索引缓冲),和立方体的 glDrawArrays 风格一致
    // ============================================================
    {
        std::vector<GLfloat> sphere;

        const int   stacks  = 36;     // 沿南北方向切成 36 层
        const int   sectors = 36;     // 沿东西方向切成 36 份
        const float radius  = 1.0f;   // 单位球,实际大小靠 lampModel.scale 控制
        const float PI      = 3.14159265358979f;
        for (int i = 0; i < stacks; ++i) {
            // 纬度:从 -PI/2(南极)扫到 +PI/2(北极)
            float lat0 = PI * (-0.5f + (float)i        / stacks);
            float lat1 = PI * (-0.5f + (float)(i + 1)  / stacks);
            for (int j = 0; j < sectors; ++j) {
                // 经度:绕一圈 0 ~ 2*PI
                float lon0 = 2.0f * PI * (float)j       / sectors;
                float lon1 = 2.0f * PI * (float)(j + 1) / sectors;

                auto P = [&](float lat, float lon) {
                    return QVector3D(radius * std::cos(lat) * std::cos(lon),
                                     radius * std::sin(lat),
                                     radius * std::cos(lat) * std::sin(lon));
                };

                QVector3D v00 = P(lat0, lon0);
                QVector3D v01 = P(lat0, lon1);
                QVector3D v11 = P(lat1, lon1);
                QVector3D v10 = P(lat1, lon0);

                // 一个网格 = 两个三角形:v00-v10-v11 和 v00-v11-v01
                QVector3D tri[6] = { v00, v10, v11,  v00, v11, v01 };
                for (const QVector3D& v : tri) {
                    sphere.push_back(v.x());
                    sphere.push_back(v.y());
                    sphere.push_back(v.z());
                }
            }
        }

        m_lampVertexCount = (int)sphere.size() / 3;

        m_lampVbo.create();
        m_lampVbo.bind();
        m_lampVbo.allocate(sphere.data(), sphere.size() * sizeof(GLfloat));
    }

    // ============================================================
    // 2. 立方体的顶点着色器:多传两个 out
    //    - FragPos:顶点变换到"世界空间"后的坐标(算光照方向要用)
    //    - Normal :法线经法线矩阵变换后的方向(判断面朝向)
    // ============================================================
    m_program.addShaderFromSourceCode(QOpenGLShader::Vertex,
        "#version 150\n"
        "in vec3 aPos;\n"                               // 顶点位置(局部空间)
        "in vec3 aNormal;\n"                            // ★ 新增:法线(局部空间)
        "out vec3 FragPos;\n"                           // 传给片段着色器:世界空间坐标
        "out vec3 Normal;\n"                            // 传给片段着色器:世界空间法线
        "uniform mat4 uModel;\n"
        "uniform mat4 uView;\n"
        "uniform mat4 uProjection;\n"
        "uniform mat3 uNormalMatrix;\n"                 // ★ 新增:法线矩阵(3x3,无平移分量)
        "void main() {\n"
        // 把顶点从局部空间 → 世界空间(供片段着色器计算"顶点→光源"方向)
        "    FragPos = vec3(uModel * vec4(aPos, 1.0));\n"
        // ★ 法线必须用法线矩阵变换,不能用 uModel 直接乘(原因见下文说明)
        "    Normal = uNormalMatrix * aNormal;\n"
        "    gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);\n"
        "}"
    );

    // ============================================================
    // 3. 立方体的片段着色器:Phong 光照的核心计算都在这
    //    三个分量:ambient(环境光)+ diffuse(漫反射)+ specular(镜面高光)
    // ============================================================
    m_program.addShaderFromSourceCode(QOpenGLShader::Fragment,
        "#version 150\n"
        "in vec3 FragPos;\n"          // 接收自顶点着色器:世界空间坐标(GPU 自动插值)
        "in vec3 Normal;\n"           // 接收自顶点着色器:世界空间法线(GPU 自动插值)
        "out vec4 FragColor;\n"
        // ---- 光源参数 uniform(由 C++ 侧 setUniformValue 传入)----
        "uniform vec3 uLightPos;\n"       // 光源位置(世界空间)
        "uniform vec3 uViewPos;\n"        // 相机位置(世界空间,算高光方向要用)
        "uniform vec3 uLightColor;\n"     // 光的颜色
        "uniform vec3 uObjectColor;\n"    // 物体基色
        "void main() {\n"
        // ========== 分量 1:环境光 Ambient ==========
        // 模拟"到处反弹的杂散光",让背光面不会纯黑
        // 强度系数 0.1 表示环境光很弱,只提供 10% 基础亮度
        "    float ambientStrength = 0.1;\n"
        "    vec3 ambient = ambientStrength * uLightColor;\n"
        // ========== 分量 2:漫反射 Diffuse ==========
        // 物体"正对光源"的部分最亮,背对的部分最暗
        "    vec3 norm = normalize(Normal);\n"  // 法线归一化(插值后长度可能不再是 1)
        // 光线方向 = 顶点位置 → 光源位置 的单位向量
        "    vec3 lightDir = normalize(uLightPos - FragPos);\n"
        // ★ 点乘的几何意义:两个单位向量的夹角余弦
        //   夹角 0° → dot=1(正对光源,最亮)
        //   夹角 90° → dot=0(擦边照到)
        //   夹角 >90° → dot<0(背光,max 钳位到 0)
        "    float diff = max(dot(norm, lightDir), 0.0);\n"
        "    vec3 diffuse = diff * uLightColor;\n"
        // ========== 分量 3:镜面高光 Specular ==========
        // 模拟"光斑":视线方向越接近"反射光方向",高光越强
        "    float specularStrength = 0.5;\n"   // 高光强度(塑料 0.5,金属更高)
        "    float shininess = 32.0;\n"         // 反光度(越大高光越小越锐利)
        // 视线方向 = 顶点位置 → 相机位置 的单位向量
        "    vec3 viewDir = normalize(uViewPos - FragPos);\n"
        // reflect(I, N):计算向量 I 关于法线 N 的反射向量
        // 注意传 -lightDir(reflect 要求 I 指向入射点)
        "    vec3 reflectDir = reflect(-lightDir, norm);\n"
        // 用 dot(viewDir, reflectDir)^shininess 制造"锐利衰减"的高光斑
        "    float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);\n"
        "    vec3 specular = specularStrength * spec * uLightColor;\n"
        // ========== 合成:三个分量相加,再乘以物体基色 ==========
        "    vec3 result = (ambient + diffuse + specular) * uObjectColor;\n"
        "    FragColor = vec4(result, 1.0);\n"
        "}"
    );

    if (!m_program.link()) {
        qDebug() << "Main Shader Link Error:" << m_program.log();
        return;
    }

    // ============================================================
    // 4. 光源指示球的着色器
    //    它是"自发光"的,不做任何光照计算,就是纯黄色
    // ============================================================
    // 光源:顶点着色器---未声明法线
    m_lampProgram.addShaderFromSourceCode(QOpenGLShader::Vertex,
        "#version 150\n"
        "in vec3 aPos;\n"
        "uniform mat4 uModel;\n"
        "uniform mat4 uView;\n"
        "uniform mat4 uProjection;\n"
        "void main() {\n"
        "    gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);\n"
        "}"
    );
    // 光源:片段着色器
    m_lampProgram.addShaderFromSourceCode(QOpenGLShader::Fragment,
        "#version 150\n"
        "out vec4 FragColor;\n"
        "void main() {\n"
        "    FragColor = vec4(1.0, 1.0, 0.0, 1.0);\n"   // 纯黄色,代表光源
        "}"
    );
    if (!m_lampProgram.link()) {
        qDebug() << "Lamp Shader Link Error:" << m_lampProgram.log();
        return;
    }

    m_timer->start(16);   // 约 60 FPS
}

void GLWidget::resizeGL(int w, int h)
{
    glViewport(0, 0, w, h);
    buildMatrices(w, h);   // 窗口变化时重建投影矩阵(aspect 必须跟随)
}

void GLWidget::buildMatrices(int w, int h)
{
    // ---- 1. 模型矩阵:立方体绕斜轴自转 ----
    m_model.setToIdentity();
    m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f);

    // ---- 2. 视图矩阵:相机姿态 ----
    m_view.setToIdentity();
    m_view.lookAt(m_cameraPos, m_cameraTarget, m_cameraUp);

    // ---- 3. 投影矩阵:透视 ----
    m_projection.setToIdentity();
    m_projection.perspective(45.0f, float(w)/float(h), 0.1f, 100.0f);

    // ---- 4. ★ 光照新增:法线矩阵 ----
    // Qt 的 QMatrix4x4 自带 normalMatrix() 方法,直接返回
    // "模型矩阵的逆转置矩阵的 3x3 子矩阵"------正是法线需要的变换
    // (底层逻辑见下文说明,不用 Qt 这个方法也可以自己算)
    m_normalMatrix = m_model.normalMatrix();
}

void GLWidget::paintGL()
{
    glClearColor(0.05f, 0.05f, 0.12f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // ============================================================
    // Pass 1:绘制被光照的立方体
    // ============================================================
    m_vbo.bind();   // 立方体自己的 VBO(位置 + 法线)
    m_program.bind();

    // 顶点属性布局:每个顶点 6 个 float,前 3 个是位置,后 3 个是法线
    int posAttr    = m_program.attributeLocation("aPos");
    int normalAttr = m_program.attributeLocation("aNormal");

    m_program.setAttributeBuffer(posAttr,    GL_FLOAT, 0,                  3, GLF_SIZE * sizeof(GLfloat));
    m_program.enableAttributeArray(posAttr);
    m_program.setAttributeBuffer(normalAttr, GL_FLOAT, 3 * sizeof(GLfloat), 3, GLF_SIZE * sizeof(GLfloat));
    m_program.enableAttributeArray(normalAttr);

    // 每帧更新自转---物体只旋转
    m_model.setToIdentity();
    m_model.rotate(m_angle, 0.5f, 1.0f, 0.0f);
    m_normalMatrix = m_model.normalMatrix();   // 法线矩阵也要跟着更新!

    // MVP + 法线矩阵
    m_program.setUniformValue("uModel",       m_model);
    m_program.setUniformValue("uView",        m_view);
    m_program.setUniformValue("uProjection",  m_projection);
    m_program.setUniformValue("uNormalMatrix", m_normalMatrix);   // ★ 3x3 矩阵

    // ★ 光照专属 uniform:光源位置、相机位置、两种颜色
    //   uViewPos 就是相机位置------因为"高光"需要知道观察者从哪看
    m_program.setUniformValue("uLightPos",    m_lightPos);
    m_program.setUniformValue("uViewPos",     m_cameraPos);
    m_program.setUniformValue("uLightColor",  m_lightColor);
    m_program.setUniformValue("uObjectColor", m_objectColor);

    glDrawArrays(GL_TRIANGLES, 0, 36);

    m_program.disableAttributeArray(posAttr);
    m_program.disableAttributeArray(normalAttr);
    m_program.release();

    // ============================================================
    // Pass 2:绘制光源指示球(独立球体 VBO + 自发光着色器)
    // ============================================================
    m_lampVbo.bind();   // ★ 换成球体专用的 VBO
    m_lampProgram.bind();

    // 球体顶点只有位置:stride = 3 * sizeof(GLfloat)
    int lampPosAttr = m_lampProgram.attributeLocation("aPos");
    m_lampProgram.setAttributeBuffer(lampPosAttr, GL_FLOAT, 0, 3, 3 * sizeof(GLfloat));
    m_lampProgram.enableAttributeArray(lampPosAttr);

    // 光源球的模型矩阵:先缩小成小圆球,再平移到光源位置
    // 注意顺序:先 scale 后 translate(矩阵乘法从右往左作用)
    QMatrix4x4 lampModel;
    lampModel.setToIdentity();
    lampModel.translate(m_lightPos);   // 平移到光源位置
    lampModel.scale(0.2f);             // 单位球缩成半径 0.2 的小圆球

    m_lampProgram.setUniformValue("uModel",      lampModel);
    m_lampProgram.setUniformValue("uView",       m_view);
    m_lampProgram.setUniformValue("uProjection", m_projection);

    glDrawArrays(GL_TRIANGLES, 0, m_lampVertexCount);   // 画一个圆球

    m_lampProgram.disableAttributeArray(lampPosAttr);
    m_lampProgram.release();
}

void GLWidget::keyPressEvent(QKeyEvent *event)
{
    const float speed = 0.3f;
    switch (event->key()) {
    case Qt::Key_W: m_cameraPos.setZ(m_cameraPos.z() - speed); break;
    case Qt::Key_S: m_cameraPos.setZ(m_cameraPos.z() + speed); break;
    case Qt::Key_A: m_cameraPos.setX(m_cameraPos.x() - speed); break;
    case Qt::Key_D: m_cameraPos.setX(m_cameraPos.x() + speed); break;
    case Qt::Key_Q: m_cameraPos.setY(m_cameraPos.y() + speed); break;
    case Qt::Key_E: m_cameraPos.setY(m_cameraPos.y() - speed); break;
    // ★ 加个实验:用 J/K 移动光源的 X 位置,直观感受"光源移动对光照的影响"
    case Qt::Key_J: m_lightPos.setX(m_lightPos.x() - 0.2f); break;
    case Qt::Key_K: m_lightPos.setX(m_lightPos.x() + 0.2f); break;
    }
    buildMatrices(width(), height());
    update();
}
相关推荐
时间的拾荒人2 小时前
Qt 文件操作详解:从基础类到读写实战
开发语言·qt·面试
扶尔魔ocy5 小时前
【qt openGL入门笔记】坐标系统和摄像机
qt·opengl
C++ 老炮儿的技术栈19 小时前
main函数之后的调用
c语言·c++·人工智能·qt·mfc·c
沫璃染墨1 天前
《Qt从零入门系列(十二):Qt文件操作详解——从QFile读写到QFileInfo与记事本实战》
开发语言·网络·c++·qt·交互·信号处理·文件
时间的拾荒人1 天前
Qt 界面布局与容器控件详解:从分组框到布局管理器
开发语言·qt·面试
王铭诗1 天前
QT界面安装
qt
程序员老陆1 天前
嵌入式 Qt:QWidgets 还是 QML / Qt Quick?
开发语言·qt
待什么青丝1 天前
【外设】之大彩串口显示屏
单片机·qt·显示器
Frank_refuel1 天前
深入理解RpcRouter(远端调用模块)
开发语言·qt