1.8.1 摄像机

一、摄像机

OpenGL本身没有摄像机的概念,但是我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种我们在移动的感觉,而不是场景在移动。

本节将会讨论如何在OpenGL中配置一个摄像机,让你能够在3D场景中自由移动,也会讨论键盘和鼠标输入,最终完成一个自定义的摄像机类。

1、摄像机/观察空间

当讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,需要它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量,实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。

1)摄像机位置

摄像机位置就是世界空间中一个指向摄像机位置的向量。把摄像机位置设置为上一节的那个位置:

QVector3D cameraPos = QVector3D(0.0f, 0.0f, 3.0f); //位置

2)摄像机方向

摄像机的方向是摄像机指向的方向。现在让摄像机指定场景原点(0, 0, 0)。用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。我们知道摄像机指向z轴负方向,但是希望方向向量指向摄像机的z轴正方向,交换相减的顺序,就会获得一个指向摄像机z轴正方向的向量:

QVector3D cameraTarget = QVector3D(0.0f, 0.0f, 0.0f);
QVector3D cameraDirection = cameraPos - cameraTarget; //方向
cameraDirection.normalize(); //单位向量

方向向量不是最好的名字,因为它实际上指向从它到目标向量的相反方向,如上图中蓝色的方向向量大概指向z轴的正方向,与摄像机实际指向的方向是相反的。

3)右轴

需要一个右向量,它代表摄像机空间的x轴的正方向。为获得右向量需要先使用一个小技巧:先定义一个上向量,把上向量和第二步的方向向量进行叉乘,两个向量叉乘的结果会同时垂直于这两个向量,因此会得到指向x轴正方向的那个向量(如果交换两个向量叉乘的顺序就会得到指向x轴负方向的向量):

QVector3D up = QVector3D(0.0f, 1.0f, 0.0f);
QVector3D cameraRight = QVector3D::crossProduct(up, cameraDirection); //叉乘,右向量
cameraRight.normalize(); //单位向量

4)上轴

现在把x轴向量和z轴向量进行叉乘,得到y轴向量

QVector3D cameraUp = QVector3D::crossProduct(cameraDirection,cameraRight); //叉乘,上向量

2、Look At

可以用3个相互垂直的轴定义一个坐标空间,可以用3个轴外加一个平移向量来创建一个矩阵,并且可以用这个矩阵乘以任何向量来将其变换到那个坐标空间,这正是LookAt矩阵所做的。

其中R是右向量,U是上向量,D是方向向量,P是摄像机位置向量(位置向量是相反的)

只需要给Qt中lookAt()函数提供一个摄像机位置、一个目标位置和一个表示世界空间中的上向量的向量,就可以得到一个观察矩阵(因为在上面四步中用摄像机位置、目标位置和上向量计算出其他向量,lookAt函数会自动计算出其他向量)

QMatrix4x4 view;
view.lookAt(cameraPos, cameraTarget, up);

下面实现摄像机在场景中旋转,摄像机的注视点保持在(0, 0, 0)。

在myopenglwidget.h文件中声明了6个QVector3D类型的变量,用于存储摄像机相关的数据;QTime类型的变量用于计时:

#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QTimer>
#include <QTime>

class MyOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core
{
    Q_OBJECT

public:
    explicit MyOpenGLWidget(QWidget *parent = nullptr);
    ~MyOpenGLWidget();

protected:
    virtual void initializeGL();
    virtual void resizeGL(int w, int h);
    virtual void paintGL();

    void keyPressEvent(QKeyEvent *event);

private slots:
    void onTimeout();

private:
    QOpenGLShaderProgram m_shaderProgram;
    QOpenGLTexture *m_textureWall;
    QOpenGLTexture *m_textureSmile;
    QOpenGLTexture *m_textureSmall;

    float mixValue = 0.5;

    QTimer m_timer;
    QTime m_time;

    QVector3D m_cameraPos;
    QVector3D m_cameraTarget;
    QVector3D m_cameraDirection;
    QVector3D m_up;
    QVector3D m_cameraRight;
    QVector3D m_cameraUp;
};

#endif // MYOPENGLWIDGET_H

更改myopenglwidget.cpp代码如下:

#include "myopenglwidget.h"
#include <QDebug>
#include <QKeyEvent>

unsigned int VBO; //顶点缓冲对象
unsigned int VAO; //顶点数组对象
unsigned int EBO; //元素缓冲对象

MyOpenGLWidget::MyOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    setFocusPolicy(Qt::StrongFocus);

    //位置
    m_cameraPos = QVector3D(0.0f, 0.0f, 3.0f);

    //方向
    m_cameraTarget = QVector3D(0.0f, 0.0f, 0.0f);
    m_cameraDirection = m_cameraPos - m_cameraTarget;
    m_cameraDirection.normalize(); //单位向量

    //右向量
    m_up = QVector3D(0.0f, 1.0f, 0.0f);
    m_cameraRight = QVector3D::crossProduct(m_up, m_cameraDirection); //叉乘,右向量
    m_cameraRight.normalize(); //单位向量

    //上向量
    m_cameraUp = QVector3D::crossProduct(m_cameraDirection, m_cameraRight); //叉乘,上向量

    connect(&m_timer, &QTimer::timeout, this, &MyOpenGLWidget::onTimeout);
    m_timer.start(100); //100ms
    m_time.start(); //开始计时
}

MyOpenGLWidget::~MyOpenGLWidget()
{
    if(m_timer.isActive())
        m_timer.stop();

    makeCurrent();
    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &EBO);
    doneCurrent();
}

void MyOpenGLWidget::onTimeout()
{
    update();
}

void MyOpenGLWidget::initializeGL()
{
    //初始化OpenGL函数
    initializeOpenGLFunctions();

    //创建VBO,并赋予ID
    glGenBuffers(1, &VBO);
    //绑定VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //顶点数据
    float vertices[] = {
         //位置                //纹理
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
    //把顶点数据复制到显存中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //创建VAO对象,并赋予ID
    glGenVertexArrays(1, &VAO);
    //绑定VAO对象
    glBindVertexArray(VAO);

    //创建EBO对象,并赋予ID
    glGenBuffers(1, &EBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    unsigned int indices[] = {
                               0, 1, 3, //第一个三角形
                               1, 2, 3 //第二个三角形
                             };
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    //位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0); //开启VAO管理的第一个属性值

    //纹理属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1); //开启VAO管理的第三个属性值

    //解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    //解绑VAO
    glBindVertexArray(0);

    //创建一个程序对象
    m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/shaders/shapes.vert");
    m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/shaders/shapes.frag");
    bool success = m_shaderProgram.link();
    if(!success)
        qDebug()<<"ERR:" << m_shaderProgram.log();

    m_shaderProgram.bind();
    m_shaderProgram.setUniformValue("vertexColor", 0.0, 1.0, 0.0, 1.0);

    m_textureWall = new QOpenGLTexture(QImage(":/images/wall.jpg").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureWall", 0); //把纹理单元传给片段着色器中的采样器

    m_textureSmile = new QOpenGLTexture(QImage(":/images/awesomeface.png").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureSmile", 1); //把纹理单元传给片段着色器中的采样器

    m_textureSmall = new QOpenGLTexture(QImage(":/images/small.png").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureSmall", 2); //把纹理单元传给片段着色器中的采样器

    m_shaderProgram.setUniformValue("mixValue", mixValue);
}

void MyOpenGLWidget::resizeGL(int w, int h)
{
    Q_UNUSED(w);

    Q_UNUSED(h);
    //glViewport(0, 0, w, h);
}

QVector<QVector3D> cubePositions=
{
    QVector3D( 0.0f, 0.0f, 0.0f),
    QVector3D( 2.0f, 5.0f, -15.0f),
    QVector3D(-1.5f, -2.2f, -2.5f),
    QVector3D(-3.8f, -2.0f, -12.3f),
    QVector3D( 2.4f, -0.4f, -3.5f),
    QVector3D(-1.7f, 3.0f, -7.5f),
    QVector3D( 1.3f, -2.0f, -2.5f),
    QVector3D( 1.5f, 2.0f, -2.5f),
    QVector3D( 1.5f, 0.2f, -1.5f),
    QVector3D(-1.3f, 1.0f, -1.5f)
};

void MyOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //设置墨绿色背景
    glEnable(GL_DEPTH_TEST); //打开深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清空

    //观察矩阵
    QMatrix4x4 view; //创建单位矩阵
    const float radius = 10.0f;
    float time = m_time.elapsed()/1000.0;
    float camX = sin(time) * radius;
    float camZ = cos(time) * radius;
    view.lookAt(QVector3D(camX, 0.0, camZ), m_cameraTarget, m_up);
    m_shaderProgram.setUniformValue("view", view);

    //投影矩阵
    QMatrix4x4 projection; //创建单位矩阵
    projection.perspective(45.0f, (float)width()/height(), 0.1f, 100.0f); //透视投影
    m_shaderProgram.setUniformValue("projection", projection);

    //绘制
    m_shaderProgram.bind(); //激活程序对象
    glBindVertexArray(VAO); //绑定VAO
    m_textureWall->bind(0); //绑定激活纹理单元0
    m_textureSmile->bind(1); //绑定激活纹理单元1
    m_textureSmall->bind(2); //绑定激活纹理单元2

    foreach(auto item, cubePositions)
    {
        //模型矩阵
        QMatrix4x4 model; //创建单位矩阵
        model.translate(item); //移动
        model.rotate(time, 1.0f, 1.f, 0.0f); //绕向量(1.0f, 1.f, 0.0f)旋转
        m_shaderProgram.setUniformValue("model", model); //传给顶点着色器
        glDrawArrays(GL_TRIANGLES, 0, 36); //绘图
    }
}

void MyOpenGLWidget::keyPressEvent(QKeyEvent *event)
{
    if(event->key() == Qt::Key_Up)
        mixValue+=0.1;
    else if(event->key() == Qt::Key_Down)
        mixValue-=0.1;
    else
        return;

    if(mixValue > 1.0)
        mixValue = 1.0;

    if(mixValue < 0.0)
        mixValue = 0.0;

    makeCurrent();
    m_shaderProgram.setUniformValue("mixValue", mixValue);
    doneCurrent();
    update();

    QOpenGLWidget::keyPressEvent(event);
}

在构造函数中初始化6个QVector3D类型的变量;同时变量m_time开始计时;

在paintGL()函数中通过更改摄像机的位置来生成不同的观察矩阵,使摄像机达到随着时间流逝围绕场景转动的效果

//观察矩阵
QMatrix4x4 view; //创建单位矩阵
const float radius = 10.0f;
float time = m_time.elapsed()/1000.0; //elapsed()函数:从start开始到现在一共经历了多少毫秒
float camX = sin(time) * radius;
float camZ = cos(time) * radius;
view.lookAt(QVector3D(camX, 0.0, camZ), m_cameraTarget, m_up);
m_shaderProgram.setUniformValue("view", view);

下面这行代码使用三角学的知识来计算每一帧的x和z坐标,预先设定圆的半径radius,QVector3D(camX, 0.0, camZ)表示圆上的一点;随着时间的流逝,会遍历圆上所有的点,这样摄像机就会绕着场景旋转了

float camX = sin(time) * radius;
float camZ = cos(time) * radius;

运行结果如下:

二、自由移动

下面用键盘上A、S、D、W键来控制摄像机移动

更改myopenglwidget.h代码如下,新定义一个方向向量 m_cameraFront:

#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QTimer>
#include <QTime>

class MyOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core
{
    Q_OBJECT

public:
    explicit MyOpenGLWidget(QWidget *parent = nullptr);
    ~MyOpenGLWidget();

protected:
    virtual void initializeGL();
    virtual void resizeGL(int w, int h);
    virtual void paintGL();

    void keyPressEvent(QKeyEvent *event);

private slots:
    void onTimeout();

private:
    QOpenGLShaderProgram m_shaderProgram;
    QOpenGLTexture *m_textureWall;
    QOpenGLTexture *m_textureSmile;
    QOpenGLTexture *m_textureSmall;

    float mixValue = 0.5;

    QTimer m_timer;
    QTime m_time;

    QVector3D m_cameraPos;
    QVector3D m_cameraTarget;
    QVector3D m_cameraDirection;
    QVector3D m_up;
    QVector3D m_cameraRight;
    QVector3D m_cameraUp;
    QVector3D m_cameraFront;
};

#endif // MYOPENGLWIDGET_H

更改myopenglwidget.cpp代码如下:

#include "myopenglwidget.h"
#include <QDebug>
#include <QKeyEvent>

unsigned int VBO; //顶点缓冲对象
unsigned int VAO; //顶点数组对象
unsigned int EBO; //元素缓冲对象

#define TIMEOUTMSEC 100

MyOpenGLWidget::MyOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    setFocusPolicy(Qt::StrongFocus);

    //位置
    m_cameraPos = QVector3D(0.0f, 0.0f, 3.0f);

    //方向
    m_cameraTarget = QVector3D(0.0f, 0.0f, 0.0f);
    m_cameraDirection = m_cameraPos - m_cameraTarget;
    m_cameraDirection.normalize(); //单位向量

    //右向量
    m_up = QVector3D(0.0f, 1.0f, 0.0f);
    m_cameraRight = QVector3D::crossProduct(m_up, m_cameraDirection); //叉乘,右向量
    m_cameraRight.normalize(); //单位向量

    //上向量
    m_cameraUp = QVector3D::crossProduct(m_cameraDirection, m_cameraRight); //叉乘,上向量
    m_cameraFront = QVector3D(0.0, 0.0, -1.0);

    connect(&m_timer, &QTimer::timeout, this, &MyOpenGLWidget::onTimeout);
    m_timer.start(TIMEOUTMSEC); //ms
    m_time.start(); //开始计时
}

MyOpenGLWidget::~MyOpenGLWidget()
{
    if(m_timer.isActive())
        m_timer.stop();

    makeCurrent();
    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &EBO);
    doneCurrent();
}

void MyOpenGLWidget::onTimeout()
{
    update();
}

void MyOpenGLWidget::initializeGL()
{
    //初始化OpenGL函数
    initializeOpenGLFunctions();

    //创建VBO,并赋予ID
    glGenBuffers(1, &VBO);
    //绑定VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //顶点数据
    float vertices[] = {
         //位置                //纹理
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
    //把顶点数据复制到显存中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //创建VAO对象,并赋予ID
    glGenVertexArrays(1, &VAO);
    //绑定VAO对象
    glBindVertexArray(VAO);

    //创建EBO对象,并赋予ID
    glGenBuffers(1, &EBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    unsigned int indices[] = {
                               0, 1, 3, //第一个三角形
                               1, 2, 3 //第二个三角形
                             };
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    //位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0); //开启VAO管理的第一个属性值

    //纹理属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1); //开启VAO管理的第三个属性值

    //解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    //解绑VAO
    glBindVertexArray(0);

    //创建一个程序对象
    m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/shaders/shapes.vert");
    m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/shaders/shapes.frag");
    bool success = m_shaderProgram.link();
    if(!success)
        qDebug()<<"ERR:" << m_shaderProgram.log();

    m_shaderProgram.bind();
    m_shaderProgram.setUniformValue("vertexColor", 0.0, 1.0, 0.0, 1.0);

    m_textureWall = new QOpenGLTexture(QImage(":/images/wall.jpg").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureWall", 0); //把纹理单元传给片段着色器中的采样器

    m_textureSmile = new QOpenGLTexture(QImage(":/images/awesomeface.png").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureSmile", 1); //把纹理单元传给片段着色器中的采样器

    m_textureSmall = new QOpenGLTexture(QImage(":/images/small.png").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureSmall", 2); //把纹理单元传给片段着色器中的采样器

    m_shaderProgram.setUniformValue("mixValue", mixValue);
}

void MyOpenGLWidget::resizeGL(int w, int h)
{
    Q_UNUSED(w);

    Q_UNUSED(h);
    //glViewport(0, 0, w, h);
}

QVector<QVector3D> cubePositions=
{
    QVector3D( 0.0f, 0.0f, 0.0f),
    QVector3D( 2.0f, 5.0f, -15.0f),
    QVector3D(-1.5f, -2.2f, -2.5f),
    QVector3D(-3.8f, -2.0f, -12.3f),
    QVector3D( 2.4f, -0.4f, -3.5f),
    QVector3D(-1.7f, 3.0f, -7.5f),
    QVector3D( 1.3f, -2.0f, -2.5f),
    QVector3D( 1.5f, 2.0f, -2.5f),
    QVector3D( 1.5f, 0.2f, -1.5f),
    QVector3D(-1.3f, 1.0f, -1.5f)
};

void MyOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //设置墨绿色背景
    glEnable(GL_DEPTH_TEST); //打开深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清空

    float time = m_time.elapsed()/1000.0;

    //观察矩阵
    QMatrix4x4 view; //创建单位矩阵
    view.lookAt(m_cameraPos, m_cameraPos + m_cameraFront, m_up);
    m_shaderProgram.setUniformValue("view", view);

    //投影矩阵
    QMatrix4x4 projection; //创建单位矩阵
    projection.perspective(45.0f, (float)width()/height(), 0.1f, 100.0f); //透视投影
    m_shaderProgram.setUniformValue("projection", projection);

    //绘制
    m_shaderProgram.bind(); //激活程序对象
    glBindVertexArray(VAO); //绑定VAO
    m_textureWall->bind(0); //绑定激活纹理单元0
    m_textureSmile->bind(1); //绑定激活纹理单元1
    m_textureSmall->bind(2); //绑定激活纹理单元2

    foreach(auto item, cubePositions)
    {
        //模型矩阵
        QMatrix4x4 model; //创建单位矩阵
        model.translate(item); //移动
        model.rotate(time, 1.0f, 1.f, 0.0f); //绕向量(1.0f, 1.f, 0.0f)旋转
        m_shaderProgram.setUniformValue("model", model); //传给顶点着色器
        glDrawArrays(GL_TRIANGLES, 0, 36); //绘图
    }
}

void MyOpenGLWidget::keyPressEvent(QKeyEvent *event)
{
    float cameraSpeed = 2.5 * TIMEOUTMSEC / 1000.0;

    switch (event->key()) {
    case Qt::Key_Up: mixValue += 0.1; break;
    case Qt::Key_Down: mixValue -= 0.1; break;
    case Qt::Key_W: m_cameraPos += cameraSpeed * m_cameraFront; break;
    case Qt::Key_S: m_cameraPos -= cameraSpeed * m_cameraFront; break;
    case Qt::Key_D: m_cameraPos += cameraSpeed * m_cameraRight; break;
    case Qt::Key_A: m_cameraPos -= cameraSpeed * m_cameraRight; break;
    default: break;
    }

    if(mixValue > 1.0)
        mixValue = 1.0;

    if(mixValue < 0.0)
        mixValue = 0.0;

    makeCurrent();
    m_shaderProgram.setUniformValue("mixValue", mixValue);
    doneCurrent();
    update();

    QOpenGLWidget::keyPressEvent(event);
}

首先,进行宏定义

#define TIMEOUTMSEC 100

更改一下定时器

m_timer.start(TIMEOUTMSEC);

方向向量初始化,使摄像机一直向前看

m_cameraFront = QVector3D(0.0, 0.0, -1.0);

更改观察矩阵,把摄像机位置设置为之前定义的m_cameraPos,方向是当前的位置加上新定义的方向向量m_cameraFront,这样能够保证无论怎么移动,摄像机都会注视着目标方向。

//观察矩阵
QMatrix4x4 view; //创建单位矩阵
view.lookAt(m_cameraPos, m_cameraPos + m_cameraFront, m_up);
m_shaderProgram.setUniformValue("view", view);

更改keyPressEvent()函数代码,使键盘上A、S、D、W键起作用

//移动步长
float cameraSpeed = 2.5 * TIMEOUTMSEC / 1000.0;

switch (event->key())
{
    case Qt::Key_Up: mixValue += 0.1; break;
    case Qt::Key_Down: mixValue -= 0.1; break;
    case Qt::Key_W: m_cameraPos += cameraSpeed * m_cameraFront; break;
    case Qt::Key_S: m_cameraPos -= cameraSpeed * m_cameraFront; break;
    case Qt::Key_D: m_cameraPos += cameraSpeed * m_cameraRight; break;
    case Qt::Key_A: m_cameraPos -= cameraSpeed * m_cameraRight; break;
    default: break;
}

2.5是固定步长,当只有固定步长时,一直按着某一个键,刷新速度快的机器移动速度就快,为了消除不同机器的影响,使移动速度都一样,使固定步长乘以定时器刷新时间(TIMEOUTMSEC是毫秒,除以1000变成秒)(定时器刷新时间可以当做机器刷新时间)

当向前或向后移动时,把位置向量加减方向向量;左右移动时,把位置向量加减右向量(对右向进行量标准化是为了匀速移动);

运行程序,使用WASD键可以控制摄像机移动,摄像机一直看着前方

三、视角移动

通过鼠标改变方向向量m_cameraFront,来改变摄像机的视角

1、欧拉角

欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉在18世纪提出,一共有3中欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),如下图所示:

  • 第一张图片展示俯仰角,俯仰角是描述如何往上或往下看的角;
  • 第二张图片展示偏航角,偏航角是描述如何往左或往右看的角;
  • 第三张图片展示滚转角,滚转角是描述如何翻滚摄像机。
  • 把三个角结合起来就能够计算3D空间中任何的旋转向量。在摄像机系统,只关心俯仰角和偏航角。

上图中把斜边边长定义为1,领边的长度是

对边边长是

这样这样获得了能够得到x和y方向长度的通用公式,它们取决于所给的角度,使用它来计算方向向量的分量:

这个三角形看起来和前面的三角形很像,所以如果我们想象自己在xz平面,看向y轴,我们可以基于第一个三角形来计算它的长度/y方向的强度(我们往上或往下看多少),从图中可以看到对于一个给定俯仰角的y值等于sinθ:

cameraFront.setY(sin(pitch*PI/180)); //先把角度转为弧度

这里只更新了y值,仔细观察x和z分量也被影响l,从三角形中我们可以看到它们的值等于:

cameraFront.setX(cos(pitch*PI/180));
cameraFront.setZ(cos(pitch*PI/180));

看看我们能否为偏航角找到需要的分量:

就像俯仰角三角形一样,我们可以看到x分量取决于cos(yaw)的值,z值同样取决于偏航角的正弦值。把这个加到前面的值中,会得到基于俯仰角和偏航角的方向向量:

cameraFront.setX(cos(yaw*PI/180) * cos(pitch*PI/180));
cameraFront.setY(sin(pitch*PI/180));
cameraFront.setZ(sin(yaw*PI/180) * cos(pitch*PI/180));

这样就有了一个可以把俯仰角和偏航角转化为用来自由旋转视角的摄像机的3维方向向量了。

2、鼠标输入

在myopenglwidget.h中重载鼠标事件:

void mouseMoveEvent(QMouseEvent *event);

在myopenglwidget.cpp的构造函数的上面定义两个全局变量

float PI = 3.1415926; //π值
QPoint deltaPos; //上次鼠标的差值

在myopenglwidget.cpp的构造函数中捕获鼠标事件

setMouseTracking(true);

mouseMoveEvent()函数代码

void MyOpenGLWidget::mouseMoveEvent(QMouseEvent *event)
{
    static float yaw = -90; //偏航角默认-90度
    static float pitch = 0;
    static QPoint lastPos(width()/2,height()/2); //上一帧鼠标的位置,初始化为屏幕中心
    
    auto currentPos = event->pos(); //当前帧的鼠标位置
    deltaPos = currentPos - lastPos; //当前帧和上一帧鼠标位置的偏移量
    lastPos = currentPos;
    
    float sensitivity = 0.1f; //灵敏度
    deltaPos *= sensitivity;
    
    yaw += deltaPos.x();
    pitch -= deltaPos.y();
    
    //限制范围
    if(pitch > 89.0f) pitch = 89.0f;
    if(pitch < -89.0f) pitch = -89.0f;

    //通过俯仰角和偏航角来计算得到真正的方向向量
    m_cameraFront.setX(cos(yaw*PI/180) * cos(pitch*PI/180));
    m_cameraFront.setY(sin(pitch*PI/180));
    m_cameraFront.setZ(sin(yaw*PI/180) * cos(pitch*PI/180));
    m_cameraFront.normalize();

    update();
}

运行程序,通过鼠标的移动可以控制摄像头向上、下、左或右转动:

3、缩放

之前的教程中视野(Field of View) 或fov定义了我们可以看到场景中多大的范围,当视野变小时,场景投影出来的空间就会减小,产生放大(Zoom In)的感觉。

在myopenglwidget.h中重载鼠标滚轮事件:

void wheelEvent(QWheelEvent *event);

在myopenglwidget.cpp的构造函数的上面定义一个全局变量

float fov = 45.0;

添加鼠标滚轮事件代码

void MyOpenGLWidget::wheelEvent(QWheelEvent *event)
{
    if(fov >= 1.0f && fov <= 75.0f)
        fov -= event->angleDelta().y()/120;//一步是120
    if(fov <= 1.0f) fov = 1.0f;
    if(fov >= 75.0f) fov = 75.0f;

    update();
}

运行程序,通过鼠标滚轮控制视野的放大或缩小:

四、摄像机类

把camera.h拷贝到项目目录下

#ifndef CAMERA_H
#define CAMERA_H

#include<QMatrix4x4>

#include <vector>

// 移动方向枚举量. 是一种抽象,以避开特定于窗口系统的输入方法
// 我们这里是WSAD
enum Camera_Movement {
    FORWARD,
    BACKWARD,
    LEFT,
    RIGHT
};

// 默认值
const float YAW         = -90.0f;
const float PITCH       =  0.0f;
const float SPEED       =  2.5f;
const float SENSITIVITY =  0.1f;
const float ZOOM        =  45.0f;


// 一个抽象的camera类,用于处理输入并计算相应的Euler角度、向量和矩阵,以便在OpenGL中使用
class Camera
{
public:
    // camera Attributes
    QVector3D Position;
    QVector3D Front;
    QVector3D Up;
    QVector3D Right;
    QVector3D WorldUp;
    // euler Angles
    float Yaw;
    float Pitch;
    // camera options
    float MovementSpeed;
    float MouseSensitivity;
    float Zoom;

    // constructor with vectors
    Camera(QVector3D position = QVector3D(0.0f, 0.0f, 0.0f), QVector3D up = QVector3D(0.0f, 1.0f, 0.0f), float yaw = YAW, float pitch = PITCH) : Front(QVector3D(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
    {
        Position = position;
        WorldUp = up;
        Yaw = yaw;
        Pitch = pitch;
        updateCameraVectors();
    }
    // constructor with scalar values
    Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) : Front(QVector3D(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
    {
        Position = QVector3D(posX, posY, posZ);
        WorldUp = QVector3D(upX, upY, upZ);
        Yaw = yaw;
        Pitch = pitch;
        updateCameraVectors();
    }

    // returns the view matrix calculated using Euler Angles and the LookAt Matrix
    QMatrix4x4 GetViewMatrix()
    {
        QMatrix4x4 theMatrix;
        theMatrix.lookAt(Position, Position + Front, Up);
        return theMatrix;
    }

    // 处理从任何类似键盘的输入系统接收的输入。接受摄像机定义枚举形式的输入参数(从窗口系统中提取)
    void ProcessKeyboard(Camera_Movement direction, float deltaTime)
    {
        float velocity = MovementSpeed * deltaTime;
        if (direction == FORWARD)
            Position += Front * velocity;
        if (direction == BACKWARD)
            Position -= Front * velocity;
        if (direction == LEFT)
            Position -= Right * velocity;
        if (direction == RIGHT)
            Position += Right * velocity;
    }

    // 处理从鼠标输入系统接收的输入。需要x和y方向上的偏移值。
    void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true)
    {
        xoffset *= MouseSensitivity;
        yoffset *= MouseSensitivity;

        Yaw   += xoffset;
        Pitch += yoffset;

        // 确保当投球超出边界时,屏幕不会翻转
        if (constrainPitch)
        {
            if (Pitch > 89.0f)
                Pitch = 89.0f;
            if (Pitch < -89.0f)
                Pitch = -89.0f;
        }

        // 使用更新的Euler角度更新前、右和上矢量
        updateCameraVectors();
    }

    // 处理从鼠标滚轮事件接收的输入。仅需要在垂直车轮轴上输入
    void ProcessMouseScroll(float yoffset)
    {
        Zoom -= (float)yoffset;
        if (Zoom < 1.0f)
            Zoom = 1.0f;
        if (Zoom > 75.0f)
            Zoom = 75.0f;
    }

private:
    // 根据相机的(更新的)Euler角度计算前矢量
    void updateCameraVectors()
    {
        // calculate the new Front vector
        float PI=3.1415926;
        QVector3D front;
        front.setX(cos(Yaw*PI/180.0) * cos(Pitch*PI/180.0));
        front.setY( sin(Pitch*PI/180.0));
        front.setZ(sin(Yaw*PI/180.0) * cos(Pitch*PI/180.0));
        front.normalize();
        Front = front;
        // also re-calculate the Right and Up vector
        Right = QVector3D::crossProduct(Front, WorldUp);
        // 标准化向量,因为向上或向下看得越多,向量的长度就越接近0,这会导致移动速度变慢。
        Right.normalize();
        Up    = QVector3D::crossProduct(Right, Front);
        Up.normalize();
    }
};
#endif

右击项目名称,选择添加现有文件

选择camera.h

更改myopenglwidget.h中的代码,包含摄像机类头文件以及创建其类对象;删除掉QVector3D类型的变量

#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QTimer>
#include <QTime>
#include "camera.h"

class MyOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core
{
    Q_OBJECT

public:
    explicit MyOpenGLWidget(QWidget *parent = nullptr);
    ~MyOpenGLWidget();

protected:
    virtual void initializeGL();
    virtual void resizeGL(int w, int h);
    virtual void paintGL();

    void keyPressEvent(QKeyEvent *event);
    void mouseMoveEvent(QMouseEvent *event);
    void wheelEvent(QWheelEvent *event);

private slots:
    void onTimeout();

private:
    QOpenGLShaderProgram m_shaderProgram;
    QOpenGLTexture *m_textureWall;
    QOpenGLTexture *m_textureSmile;
    QOpenGLTexture *m_textureSmall;

    float mixValue = 0.5;

    QTimer m_timer;
    QTime m_time;

    Camera m_camera;
};

#endif // MYOPENGLWIDGET_H

更改myopenglwidget.cpp中的代码

#include "myopenglwidget.h"
#include <QDebug>
#include <QKeyEvent>

unsigned int VBO; //顶点缓冲对象
unsigned int VAO; //顶点数组对象
unsigned int EBO; //元素缓冲对象

#define TIMEOUTMSEC 100

QPoint deltaPos;

MyOpenGLWidget::MyOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    setFocusPolicy(Qt::StrongFocus);
    setMouseTracking(true);

    m_camera.Position = QVector3D(0,0,3.0);

    connect(&m_timer, &QTimer::timeout, this, &MyOpenGLWidget::onTimeout);
    m_timer.start(TIMEOUTMSEC); //100ms
    m_time.start(); //开始计时
}

MyOpenGLWidget::~MyOpenGLWidget()
{
    if(m_timer.isActive())
        m_timer.stop();

    makeCurrent();
    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &EBO);
    doneCurrent();
}

void MyOpenGLWidget::onTimeout()
{
    update();
}

void MyOpenGLWidget::initializeGL()
{
    //初始化OpenGL函数
    initializeOpenGLFunctions();

    //创建VBO,并赋予ID
    glGenBuffers(1, &VBO);
    //绑定VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //顶点数据
    float vertices[] = {
         //位置                //纹理
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
    //把顶点数据复制到显存中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //创建VAO对象,并赋予ID
    glGenVertexArrays(1, &VAO);
    //绑定VAO对象
    glBindVertexArray(VAO);

    //创建EBO对象,并赋予ID
    glGenBuffers(1, &EBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    unsigned int indices[] = {
                               0, 1, 3, //第一个三角形
                               1, 2, 3 //第二个三角形
                             };
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    //位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0); //开启VAO管理的第一个属性值

    //纹理属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1); //开启VAO管理的第三个属性值

    //解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    //解绑VAO
    glBindVertexArray(0);

    //创建一个程序对象
    m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/shaders/shapes.vert");
    m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/shaders/shapes.frag");
    bool success = m_shaderProgram.link();
    if(!success)
        qDebug()<<"ERR:" << m_shaderProgram.log();

    m_shaderProgram.bind();
    m_shaderProgram.setUniformValue("vertexColor", 0.0, 1.0, 0.0, 1.0);

    m_textureWall = new QOpenGLTexture(QImage(":/images/wall.jpg").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureWall", 0); //把纹理单元传给片段着色器中的采样器

    m_textureSmile = new QOpenGLTexture(QImage(":/images/awesomeface.png").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureSmile", 1); //把纹理单元传给片段着色器中的采样器

    m_textureSmall = new QOpenGLTexture(QImage(":/images/small.png").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureSmall", 2); //把纹理单元传给片段着色器中的采样器

    m_shaderProgram.setUniformValue("mixValue", mixValue);
}

void MyOpenGLWidget::resizeGL(int w, int h)
{
    Q_UNUSED(w);

    Q_UNUSED(h);
    //glViewport(0, 0, w, h);
}

QVector<QVector3D> cubePositions=
{
    QVector3D( 0.0f, 0.0f, 0.0f),
    QVector3D( 2.0f, 5.0f, -15.0f),
    QVector3D(-1.5f, -2.2f, -2.5f),
    QVector3D(-3.8f, -2.0f, -12.3f),
    QVector3D( 2.4f, -0.4f, -3.5f),
    QVector3D(-1.7f, 3.0f, -7.5f),
    QVector3D( 1.3f, -2.0f, -2.5f),
    QVector3D( 1.5f, 2.0f, -2.5f),
    QVector3D( 1.5f, 0.2f, -1.5f),
    QVector3D(-1.3f, 1.0f, -1.5f)
};

void MyOpenGLWidget::paintGL()
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //设置墨绿色背景
    glEnable(GL_DEPTH_TEST); //打开深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清空

    float time = m_time.elapsed()/1000.0;

    //观察矩阵
    QMatrix4x4 view; //创建单位矩阵
    view = m_camera.GetViewMatrix();
    m_shaderProgram.setUniformValue("view", view);

    //投影矩阵
    QMatrix4x4 projection; //创建单位矩阵
    projection.perspective(m_camera.Zoom, (float)width()/height(), 0.1, 100);
    m_shaderProgram.setUniformValue("projection", projection);

    //绘制
    m_shaderProgram.bind(); //激活程序对象
    glBindVertexArray(VAO); //绑定VAO
    m_textureWall->bind(0); //绑定激活纹理单元0
    m_textureSmile->bind(1); //绑定激活纹理单元1
    m_textureSmall->bind(2); //绑定激活纹理单元2

    foreach(auto item, cubePositions)
    {
        //模型矩阵
        QMatrix4x4 model; //创建单位矩阵
        model.translate(item); //移动
        model.rotate(time, 1.0f, 1.f, 0.0f); //绕向量(1.0f, 1.f, 0.0f)旋转
        m_shaderProgram.setUniformValue("model", model); //传给顶点着色器
        glDrawArrays(GL_TRIANGLES, 0, 36); //绘图
    }
}

void MyOpenGLWidget::keyPressEvent(QKeyEvent *event)
{
    //移动步长
    //float cameraSpeed = 2.5 * TIMEOUTMSEC / 1000.0;
    float deltaTime = TIMEOUTMSEC/1000.0;

    switch (event->key())
    {
        case Qt::Key_Up: mixValue += 0.1; break;
        case Qt::Key_Down: mixValue -= 0.1; break;
        case Qt::Key_W: m_camera.ProcessKeyboard(FORWARD, deltaTime); break;
        case Qt::Key_S: m_camera.ProcessKeyboard(BACKWARD, deltaTime); break;
        case Qt::Key_D: m_camera.ProcessKeyboard(RIGHT, deltaTime); break;
        case Qt::Key_A: m_camera.ProcessKeyboard(LEFT, deltaTime); break;
        default: break;
    }

    if(mixValue > 1.0)
        mixValue = 1.0;

    if(mixValue < 0.0)
        mixValue = 0.0;

    makeCurrent();
    m_shaderProgram.setUniformValue("mixValue", mixValue);
    doneCurrent();
    update();

    QOpenGLWidget::keyPressEvent(event);
}

void MyOpenGLWidget::mouseMoveEvent(QMouseEvent *event)
{
    static QPoint lastPos(width()/2,height()/2); //上一帧鼠标的位置,初始化为屏幕中心

    auto currentPos = event->pos(); //当前帧的鼠标位置
    deltaPos = currentPos - lastPos; //当前帧和上一帧鼠标位置的偏移量
    lastPos = currentPos;

    m_camera.ProcessMouseMovement(deltaPos.x(), -deltaPos.y());

    update();
}

void MyOpenGLWidget::wheelEvent(QWheelEvent *event)
{
    m_camera.ProcessMouseScroll(event->angleDelta().y()/120);

    update();
}

运行程序,键盘WASD控制摄像机移动,鼠标控制摄像机转向,鼠标滚轮控制缩放

注:观看OpenGL中文官网(https://learnopengl-cn.github.io/)和阿西拜的现代OpenGL入门(https://ke.qq.com/course/3999604#term_id=104150693)学习OpenGL

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/499424.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Web APIs

文章目录 Web APIs1. DOM1.1 介绍DOM 树DOM 节点document 1.2 获取DOM对象1.3 操作元素内容1.4 操作元素属性常用属性修改控制样式属性操作表单元素属性自定义属性 1.5 间歇函数1.6 事件事件监听事件类型事件处理程序 1.7 事件类型鼠标事件键盘事件焦点事件文本框输入事件 1.8 …

【JavaScript】数组 ③ ( JavaScript 数组长度 | 修改数组长度 | 数组案例 )

文章目录 一、JavaScript 数组长度1、数组长度2、修改数组长度 二、数组案例1、求数组元素平均值2、求数组元素最大值 一、JavaScript 数组长度 1、数组长度 在 JavaScript 中 , 数组长度 可以通过 数组变量的 length 属性 获取 , 该属性 返回 数组中的元素数量 , 也就是 数组长…

【Java】ArrayList数组的扩容机制 jdk1.8

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 ArrayList和普通数组不同&#xff0c;ArrayList支持动态扩容&#xff0c;那么ArrayList到底是如何扩容的呢&#xff1f;你又是否知道ArrayList数组的初始长度是多少呢&#xff1f; 在开始介绍之前&#xff0c;我们要先介…

【IDEA+通义灵码插件】实现属于你的大模型编程助手

1、前言 大模型到底该以一种什么方式落地&#xff0c;从而嵌入我们的工作当中&#xff0c;助力我们工作效率的提升&#xff0c;其实最好的方式也许就是虚拟助手的方式&#xff0c;就像钢铁侠的"贾维斯"一样&#xff0c;随叫随到能回答问题&#xff0c;能自动的解决一…

python函数参数中独立星号*的作用

python函数中间有一个&#xff08;&#xff09;分隔&#xff0c;星号后面为*命名关键字参数&#xff0c;星号本身不是参数**。命名关键字参数&#xff0c;在函数调用时必须带参数名字进行调用。如下例子&#xff1a;

【Golang入门教程】Go语言变量的初始化

文章目录 强烈推荐引言举例多个变量同时赋值总结强烈推荐专栏集锦写在最后 强烈推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站:人工智能 推荐一个个人工作&#xff0c;日常中比较常…

深入剖析哈希表:以Java中的HashMap为例

哈希表是一种非常高效的数据结构&#xff0c;它允许我们以接近常数的时间复杂度进行插入、删除和查找操作。在Java中&#xff0c;HashMap类是实现哈希表的一个非常流行的工具。本文将深入探讨哈希表的工作原理&#xff0c;并通过Java代码来展示HashMap的使用和内部机制。 一、…

Linux 动静态库的制作,使用和加载

Linux 动静态库的制作,使用和加载 一.前置说明1.mylib.h2.mylib.c3.mymath.h mymath.c4.如何制作库 二.动静态库的制作1.静态库的制作1.制作2.使用一下静态库,验证是否成功打包 2.动态库的制作1.编译.c源文件文件生成.o目标文件2.打包生成动态库3.编写makefile文件,自动化制作动…

基于Springboot+vue的鲜花销售商城网站

摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;鲜花销售商城当然也不能排除在外。鲜花销售商城是以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#x…

多线程的学习1

多线程 线程是操作系统能够进入运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。 进程&#xff1a;是程序的基本执行实体。 并发&#xff1a;在同一个时刻&#xff0c;有多个指令在单个CPU上交替执行。 并行&#xff1a;在同一时刻&#xff0c…

程序汪若依微服务华为云Linux部署保姆教程

若依官方有3个版本&#xff0c;程序汪以前已经出了对应的安装部署视频教程 单应用版本 前后分离版本 微服务版本 本视频是若依微服务版本&#xff0c;如果基础的环境软件都不会安装建议看下程序汪的单应用和前后端分离版本教程&#xff0c; 欢迎点击进入 &#xff08;单应…

全局UI方法-弹窗二-列表选择弹窗(ActionSheet)

1、描述 定义列表弹窗 2、接口 ActionSheet.show(value:{ title: string | Resource, message: string | Resource, autoCancel?: boolean, confrim?: {value: string | Resource, action: () > void }, cancel?: () > void, alignment?: DialogAlignment, …

Lightguide

Resolve Assembly Challenges with Projected AR Workflows The future of assembly is here. It’s not just about automation, it’s about empowering your workforce with the best possible digital toolset. LightGuide AR is a powerful addition to your Industry 4.…

基于单片机的便携式瓦斯检测仪系统设计

**单片机设计介绍&#xff0c;基于单片机的便携式瓦斯检测仪系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的便携式瓦斯检测仪系统设计是一个针对煤矿等工业环境中瓦斯气体浓度检测的重要项目。以下是该设计…

C++template之类模版进一步了解

前言&#xff1a;这一篇是在我的上一篇文章的基础上&#xff0c;再进一步所写的。 链接&#xff1a;CTemplate&#xff1c;&#xff1e;模版的介绍及深度解析-CSDN博客 一、类模板实例化 1.非类型模版参数 类型模版参数&#xff1a;就是跟在 class后面或者typename后的类型 非…

Java NIO和IO之间的区别

前言 NIO&#xff08;New IO&#xff09;&#xff0c;这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的&#xff0c;但实现方式不同&#xff0c;NIO主要用到的是块&#xff0c;所以NIO的效率要比IO高很多。在Java API中提供了两套NIO&#xff0c;一套是针对标准输入输出…

WebPack的使用及属性配、打包资源

WebPack(静态模块打包工具)(webpack默认只识别js和json内容) WebPack的作用 把静态模块内容压缩、整合、转译等&#xff08;前端工程化&#xff09; 1️⃣把less/sass转成css代码 2️⃣把ES6降级成ES5 3️⃣支持多种模块文件类型&#xff0c;多种模块标准语法 export、export…

企业如何有效防止员工偷懒磨洋工?

在当今竞争激烈的商业环境中&#xff0c;企业要想保持竞争力和提高效益&#xff0c;就必须确保员工不偷懒不磨洋工。然而&#xff0c;要有效防止员工偷懒磨洋工并非易事&#xff0c;需要企业采取一系列措施来引导和监督员工的工作状态。 下面就分享三个有效防止员工偷懒磨洋工…

unity3d for web

时光噶然 一晃好多年过去了&#xff08;干了5年的u3d游戏&#xff09;&#xff0c;记得最后一次使用的版本好像是 unity 2017。 那个是 unity3d for webgl 还需要装个插件。用起来很蛋疼。 最近做一个小项目 在选择是用 Layabox 还是 cocosCreate 的时候 我想起了老战友 Uni…

DeepMind终结大模型幻觉?标注事实比人类靠谱、还便宜20倍,全开源

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了人工智能中文站https://ai.weoknow.com 每天给大家更新可用的国内可用chatGPT资源​ 发布在https://it.weoknow.com 更多资源欢迎关注 ​ DeepMind 这篇论文一出&#xff0c;人类标注者的饭碗也要被砸了吗&a…