OpenGL学习随笔(五)——2022.2.7

        通过前面的学习,已经了解了OpenGL渲染的主要流程和基础的数学知识,接下来继续学习如何管理3D图形数据,在本回中将会绘制一个立方体。

一、缓冲区和顶点属性

        要想绘制一个对象,它的顶点数据需要被发送给顶点着色器。在C++/OpenGl程序中,通常会把顶点数据在C++端放一个缓冲区中,并把这个缓冲区和着色器中声明的顶点属性相关联。

        所有的缓冲区通常在程序开始的时候统一创建。在OpenGL中,缓冲区被包含在顶点缓冲对象(Vertex Buffer Object, VBO)中,VBO在C++/OpenGL应用程序中被声明和实例化。一个场景可能需要很多个VBO,通常我们在初始化的阶段生成并填充若干个VBO,方便后续直接使用。

        缓冲区使用特定的方式和顶点属性交互。当glDrawArrays()被执行时,缓冲区中的数据开始流动,从缓冲区的开头开始,按顺序流过顶点着色器,顶点着色器对每个顶点执行一次。3D空间中 的顶点需要3个数值,所有着色器中的顶点属性常常会以vec3类型接收到这3个值,然后,对缓冲区中的每组这3个值,着色器会被调用。

OpenGL学习随笔(五)——2022.2.7

        在OpenGL中还有一种相关的结构,叫做顶点数组对(Vertex Array Object, VAO),OpenGL3.0版本中引入了VAO,作为一种组织缓冲区的方法,让缓冲区在复杂场景中更容易操作,OpenGL要求至少创建一个VAO。

        举个栗子,假设我们要显示两个对象。在C++端,我们可以声明一个VAO和两个VBO,

GLuint vao[1];//OpenGL要求这些数值以数组的形式指定
GLuint vbo[2];
.......

glGenVertexArrays(1,vao);
glBindVertexArray(vao[0]);
glGenBuffers(2,vbo);

        glGenVertexArrays()和glGenBuffers()这两个OpenGL命令分别创建VAO和VBO,并返回它们的整数ID,把这些ID存入整形数组vao和vbo中。这两个命令的参数分别为要创建ID的数目和用来保存返回ID的数组。glBindVertexArray()命令的目的是将指定的VAO标记为活跃,这样生成的缓冲区就会和这个VAO相关连。

        每个缓冲区需要有在顶点着色器中声明的相应的顶点属性变量。顶点属性通常是顶点着色器中首先被声明的变量。如:

layout (location = 0) in vec3 position

        命令中的“layout(location = 0)"这部分叫做"layout修饰符“,也就是我们把顶点属性和特定缓冲区关联起来的方法,并且这个顶点属性的识别号为0。关键字"in"的意思是输入(input),表示这个顶点属性将会从缓冲区中接收数值,”vec3"的意思是着色器每次调用会抓到3个浮点类型的值(分别表示X、Y、Z,它们组成一个顶点数据),变量的名字是position。

        把一个模型的顶点加载到缓冲区(VBO)的方法取决于模型的顶点数据存储在哪里,现在,假设我们想要绘制一个立方体,并且假定我们的立方体的顶点数据在C++/OpenGl应用程序中的数组中直接指定。在这种情况下,步骤如下:

  1. 将这些数据值复制到之前生成的两个缓冲区中的一个中。为此,我们用glBindBuffer()命令将选定的缓冲区(如第零个缓冲区)标记为活跃。
  2. 使用glBufferData()命令将包含顶点数据的数组复制进活跃缓冲区中(第零个缓冲区)。

        假设顶点数据存储在名为vPosition的浮点类型数组中,一下代码会将这些值复制到第零个缓冲区中。 

glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
glBufferData(GL_ARRAY_BUFFER,sizeof(vPosition),vPosition,GL_STATIC_DRAW);

        接下来,我们将缓冲区中的值发送到着色器中的顶点属性。通过以下3个步骤来实现:

  1. 如上,使用glBindBuffer()命令标记选定缓冲区为活跃
  2. 将活跃缓冲区与着色器中的顶点属性相关联。

启用顶点属性。以下代码段实现这些步骤。

glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);//第0个缓冲区被标记为活跃。
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,0);//将第0个属性关联到缓冲区
glEnableVertexAttribArray(0);//启用第0个顶点属性

现在,当执行glDrawArrays()时,第0个VBO中的数据将被传送给拥有位置0的layout修饰符的顶点属性中。总结一下,绘制一个对象,其顶点数据需要被发送给顶点着色器并绘制的步骤如下:

  1. 创建一个缓冲区
  2. 将顶点数据复制到缓冲区
  3. 启用包含了顶点数据的缓冲区
  4. 将这个缓冲区和一个顶点属性相关联
  5. 启用这个顶点属性
  6. 使用glDrawArrays()绘制对象

二、统一变量

        要想渲染一个3D场景,需要构建合适的变换矩阵(如上回所述),并将变换矩阵应用于模型的每个顶点,在顶点着色器中应用所需的矩阵运算是最有效的,并且习惯上会将这些矩阵发送给着色器中的同一变量。

        使用“uniform"关键字在着色器中声明统一变量。如下:

uniform mat4 mv_matrix;

uniform mat4 proj_matrix;

         关键字mat4表示这些4×4矩阵。将保存模型-视图的矩阵变量命名为mv_matrix,将用来保存投影矩阵的变量命名为proj_matrix。

        将数据发送到统一变量需要以下步骤:

  1. 获取统一变量的引用
  2. 将指向所需数值的指针与获取的统一变量引用相关联。

        在我们的立方体例子中,假设链接的渲染程序保存在名为”renderingProgram"的变量中,并已经利用GLM工具构建了模型-视图矩阵mvMat和投影矩阵pMat,以下代码表示将模型—视图矩阵和投影矩阵发送到两个统一变量mv_matrix和proj_matrix中。

mvLoc = glGetUniformLocation(renderingProgram,"mv_matrix");//获取着色器程序中统一变量的位置

projLoc = glGetUniformLocation(renderingProgram,"proj_matrix");

glUniformMatrix4fv(mvLoc, 1, GL_FALSE,glm::value_ptr(mvMat));//将矩阵数据发送到统一变量中
//glm::value_ptr()返回对矩阵数据的引用
glUniformMatrix4fv(projLoc,1,GL_FALSE,glm::value_ptr(pMat));

 三、顶点属性插值

        我们已经知道,在片段着色器光栅化之前,由顶点定义的图元(如三角形)被转化为片段。光栅化过程会线性插值顶点属性值,以便显示的像素能无缝连接建模的曲面。相比之下,统一变量的行为类似于初始化过程的常量,并且在每次顶点着色器调用中保持不变。统一变量本身不是插值的,无论有多少顶点,它始终包含相同的值。

        在顶点着色器中看到的顶点属性被声明为“in”,表示它们从缓冲区接收值,顶点属性还可以声明为“out"这意味着它们会将值发送给管线的下一阶段。没有必要为顶点位置声明一个“out"变量,在OpenGL中有一个内置的vec4变量用于此目的——gl_Position。在顶点着色器中,我们将矩阵变换应用于传入的顶点,并将结果返回给gl_Position:

gl_Position = proj_matrix * mv_matrix * position;

        然后,变换后的顶点将自动输出到光栅着色器,最终将相应的像素发送给片段着色器。在glDrawArrays()函数中指定GL_TRIANGLES时,光栅化是逐个三角形完成的。首先沿着连接顶点的线开始插值,然后通过沿着连接边缘像素水平线插来填充三角形。 

四、第一个3D程序——3D立方体

        渲染立方体需要用到矩阵的变换,需要自己引入GLM库,方法比较简单,直接到官网上下载GLM库,解压后将内部的glm文件夹直接放到QT工程文件下即可。

mywidget.h

#ifndef MYWIDGET_H
#define MYWIDGET_H

#define numVAOs 1
#define numVBOs 2

#include<GL/glew.h>  //该头文件一定要在最前面
#include<QOpenGLWidget>
#include<QOpenGLFunctions>
#include<qopengl.h>
#include<QOpenGLBuffer>
#include<QOpenGLShader>
#include<QOpenGLShaderProgram>
#include<GL/glfw3.h>
#include<GL/gl.h>
#include<GL/glu.h>
#include<glm/glm.hpp>
#include<glm/gtc/type_ptr.hpp>
#include<glm/gtc/matrix_transform.hpp>
#include<iostream>
#include<cmath>
#include<string>
#include<fstream>
#include<QTimer>
using namespace std;

//继承OpenGLWidget,重写initializeGL(),paintGL(),resizeGL()三个函数即可绘制OpenGL图元
class MyWidget : public QOpenGLWidget,protected QOpenGLFunctions
{
public:
    MyWidget(QWidget *parent);
    GLuint createShaderProgram();//GLuint相当于C++里的unsinged int类型
    void setupVertices(void);
public slots://定义槽函数
    void animate();

protected:
    void initializeGL() override;
    void paintGL() override;
    void resizeGL(int width, int height) override;
private:
    float cameraX,cameraY,cameraZ;
    float cubeLocX,cubeLocY,cubeLocZ;
    GLuint renderingProgram;
    GLuint vao[numVAOs];
    GLuint vbo[numVBOs];

    GLuint mvLoc,projLoc;
    int width,height;
    float aspect;
    glm::mat4 pMat,vMat,mMat,mvMat,tMat,rMat;


    float x = 0.0f;
    float inc = 0.02f;
};

#endif // MYWIDGET_H

mywidget.cpp

#include "mywidget.h"
MyWidget::MyWidget(QWidget *parent)
{
    Q_UNUSED(parent);
    QSurfaceFormat format = QSurfaceFormat::defaultFormat();
    format.setProfile(QSurfaceFormat::CoreProfile);
    format.setVersion(3, 3);
    QSurfaceFormat::setDefaultFormat(format);

}

void MyWidget::initializeGL()
{
    initializeOpenGLFunctions();
    glClearColor(0.0,0.0,0.0,1.0);//设置背景色为黑色
    glClear(GL_COLOR_BUFFER_BIT);
    renderingProgram = createShaderProgram();
    cameraX = 0.0f;//初始化相机位置
    cameraY = 0.0f;
    cameraZ = 8.0f;

    cubeLocX = 0.0f;//初始化立方体的位置
    cubeLocY = -2.0f;
    cubeLocZ = 0.0f;

    setupVertices();
}
void MyWidget::paintGL()
{
    //每帧都清除深度缓冲区并填充背景颜色为黑色
    glClearColor(0.0,0.0,0.0,1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    //启用着色器,在GPU上安装GLSL代码
    glUseProgram(renderingProgram);

    //绘制MV矩阵和投影矩阵的统一变量
    mvLoc = glGetUniformLocation(renderingProgram,"mv_matrix");
    projLoc = glGetUniformLocation(renderingProgram,"proj_matrix");

    //构建透视矩阵
    //glfwGetFramebufferSize((GLFWwindow*)this,&width,&height);
    width = 1000;height = 800;
    aspect = (float)width / (float)height;
    pMat = glm::perspective(1.0472f,aspect,0.1f,1000.0f);//1.0472rad = 60度

    //构建视图矩阵
    vMat = glm::translate(glm::mat4(1.0f),glm::vec3(-cameraX,-cameraY,-cameraZ));
    //静态立方体
    //mMat = glm::translate(glm::mat4(1.0f),glm::vec3(cubeLocX,cubeLocY,cubeLocZ));
    //动态立方体,利用平移和旋转矩阵实现动画
    x += inc;
    tMat = glm::translate(glm::mat4(1.0f),glm::vec3(sin(0.35*x)*2.0f,cos(0.52*x)*2.0f,sin(0.7*x)*2.0f));
    rMat = glm::rotate(glm::mat4(1.0f),1.75f*x,glm::vec3(0.0f,1.0f,0.0f));
    rMat = glm::rotate(rMat,1.75f*x,glm::vec3(1.0f,0.0f,0.0f));
    rMat = glm::rotate(rMat,1.75f*x,glm::vec3(0.0f,0.0f,1.0f));
    mMat = tMat * rMat;
    mvMat = vMat * mMat;

    //将透视矩阵和MV矩阵复制给相应的统一变量
    glUniformMatrix4fv(mvLoc,1,GL_FALSE,glm::value_ptr(mvMat));
    glUniformMatrix4fv(projLoc,1,GL_FALSE,glm::value_ptr(pMat));

    //将VBO关联给顶点着色器中的相应的顶点属性
    glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,0);
    glEnableVertexAttribArray(0);


    //调整OpenGL设置,绘制模型
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glDrawArrays(GL_TRIANGLES,0,36);
}
void MyWidget::resizeGL(int width, int height)
{

}

GLuint MyWidget::createShaderProgram()
{

    //顶点着色器
    const char *vshaderSource =
        "#version 430 \n"
        "layout (location = 0) in vec3 position; \n"
         "uniform mat4 mv_matrix; \n"
        "uniform mat4 proj_matrix; \n"
        "out vec4 varyingColor; \n"
        "void main(void) \n"
        "{ gl_Position = proj_matrix * mv_matrix *vec4(position,1.0); \n"
        "varyingColor = vec4(position,1.0)*0.5 + vec4(0.5,0.5,0.5,0.5);}";

    //顶点沿着管线移动到光栅着色器,它们会在这里被转化为像素(片段)位置,最终这些像素(片段)到达片段着色器
    //片段着色器的目的就是将要展示的像素赋予RGB颜色
    //"out"标签表明color变量是输出变量
    //此处vec4前三个元表示RGB颜色,第四个元表示不透明度
    const char *fshaderSource =
        "#version 430 \n"
        "in vec4 varyingColor; \n"
        "out vec4 color; \n"
        "uniform mat4 mv_matrix; \n"
        "uniform mat4 proj_matrix; \n"
        "void main(void) \n"
        "{ color = varyingColor;}";

    //调用glCreateShader(parameter)创建类型为parameter的空着色器
    //创建每个着色器对象后会返回一个整数ID作为后面引用它们的序号
    GLuint vShader = glCreateShader(GL_VERTEX_SHADER);//GLuint相当于“unsigned int"
    GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);

    //glShaderSource将GLSL代码从字符串载入空着色器对象中
    //四个参数:1、存放着色器的着色器对象,2、着色器源代码中的字符串数量,3、包含源代码的字符串指针,4、
    glShaderSource(vShader,1,&vshaderSource,NULL);
    glShaderSource(fShader,1,&fshaderSource,NULL);

    //glCompileShader编译着色器
    glCompileShader(vShader);
    glCompileShader(fShader);

    //创建程序对象,并存储指向它的整数ID
    //OpenGL的程序对象包含一系列编译过的着色器,使用glAttachShader将着色器加入程序对象
    //之后使用glLinkProgram来请求GLSL编译器来确保它们的兼容性。
    GLuint vfProgram  = glCreateProgram();
    glAttachShader(vfProgram,vShader);
    glAttachShader(vfProgram,fShader);
    glLinkProgram(vfProgram);

    return vfProgram;


}

void MyWidget::setupVertices()
{
    float vertexPositions[108] = {//该数组包含了立方体的36个顶点,利用三角形构建的立方体,立方体每个面由2个三角形组成
        -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f,
        1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f,
        1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f,
        1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f,
        1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
        -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f,1.0f,
        -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f,
        -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f,
        -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f,
        1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f,
        -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f,
        1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f,
    };

    //创建VAO并设置为活跃状态,不用这两行程序也可运行
    glGenVertexArrays(1,vao);
    glBindVertexArray(vao[0]);
    //创建VBO并设置为活跃状态
    glGenBuffers(numVBOs,vbo);
    glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
    //将顶点数据复制到缓存中
    glBufferData(GL_ARRAY_BUFFER,sizeof (vertexPositions),vertexPositions,GL_STATIC_DRAW);


}
void MyWidget::animate()
{
    this->update();//调用update时会自动调用PaintGL函数重绘
}

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include"mywidget.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
    MyWidget *my_widget;
};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<QTimer>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    //创建OpenGLWidget对象
    my_widget = new MyWidget(this);
    //将mainWindow界面设置为OpenGLWidget
    setCentralWidget( my_widget );
    //设置界面的大小
    resize( 1000, 800 );

    //设置计时器并连接槽函数
    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, my_widget, &MyWidget::animate);
    timer->start(10);//设置重绘的时间间隔
    
}

MainWindow::~MainWindow()
{
    delete ui;
}

main.cpp

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

 运行结果

OpenGL学习随笔(五)——2022.2.7

上一篇:AI利用3D查找器绘制垃圾桶图标


下一篇:WebGPU 中消失的 VAO