Qt与OpenGL编程 - Hello三角形

OpenGL是一个跨平台的GPU渲染库,Qt对OpenGL做了一部分的封装,可以使我们更加方便的使用OpenGL。

关于OpenGL更多的相关学习内容可以参考(比较全和详细的OpenGL教程):
LearnOpenglcn


1. 渲染管线

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。

3D坐标转为2D坐标的处理过程是由OpenGL的 图形渲染管线 (Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。

图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。 所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心, 它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做 着色器 (Shader)。 着色器是由 GLSL 语言(OpenGL Shading Language)编写而成,他们运行在GPU上,从而节省了CPU的运行时间。

下图是OpenGL渲染管线的流程,其中蓝色部分表示我们可以 自定义的Shader

  • 顶点着色器 : 它是图像渲染管线的第一部分,把一个单独的顶点作为输入。它主要用做坐标转换。
  • 图元装配 : 该阶段会把顶点着色器输出的所有顶点作为输入,并把所有的点装配成指定的图元。(比如三角形)
  • 几何着色器 : 该阶段会把 图元装配 的输出的顶点作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
  • 光栅化 : 这个阶段它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
  • 片段着色器 :这个阶段的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
  • 测试与混合阶段 :这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

一般情况下我们只需要自定义 顶点着色器片段着色器 就可以了, 可以使用默认的几何着色器。在现代OpenGL中, 我们必须至少定义一个 顶点着色器片段着色器 , 因为GPU中没有默认的顶点/片段着色器。


2. 标准化设备坐标

标准化设备坐标(Normalized Device Coordinates, NDC),标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。

标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。

OpenGL的坐标系使用的是右手坐标系,Z轴指向屏幕外。DirectX使用的是左手坐标系。(本篇文章绘制三角形不需要考虑Z轴,因为设置的顶点都在相同的Z轴,可以视为一个平面内)


3. 关于QOpenGLWidget

Qt中提供了类 QOpenGLWidget 可以方便我们使用OpenGL。

使用方法如下:

  • 重写虚函数 initializeGL : 设置OpenGL资源和状态。在第一次调用resizeGL()或paintGL()之前调用一次。
  • 重写虚函数 resizeGL : 设置OpenGL视图、投影等。每当小部件被调整大小时(当它第一次显示时也会被调用,因为所有新创建的小部件都会自动获得一个调整大小事件)。
  • 重写虚函数 paintGL :渲染OpenGL场景。每当Widget需要更新时调用。

QOpenglWidget 内部会管理一个 QOpenGLContext , 当调用上面几个函数的时候会自动 makeCurrent() 函数,确保运行在当前OpenGL上下文。 如果在非上述的函数中调用OpenGL的API,则需要手动去执行 makeCurrent() 这个函数。

如果想调用OpenGL的相关API,有两种方法:

  • 第一种是继承QOpenGLFunctions,然后就可以直接调用OpenGL的函数了。
  • 第二种是 使用 QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions() 的方式,然后再调用OpenGL的API。
class MyGLWidget : public QOpenGLWidget
{
public:
    MyGLWidget(QWidget *parent) : QOpenGLWidget(parent) { }

protected:
    void initializeGL()
    {
        // Set up the rendering context, load shaders and other resources, etc.:
        QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
        f->glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        ...
    }

    void resizeGL(int w, int h)
    {
        // Update projection matrix and other size related settings:
        m_projection.setToIdentity();
        m_projection.perspective(45.0f, w / float(h), 0.01f, 100.0f);
        ...
    }

    void paintGL()
    {
        // Draw the scene:
        QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
        f->glClear(GL_COLOR_BUFFER_BIT);
        ...
    }

};

4. VBO

在OpenGL中,我们要顶点数据传给显存。我们通过 顶点缓冲对象 (Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。

(1)创建VBO。

函数 glGenBuffers 用于创建一个缓存对象。(通过ID管理并操作这个对象)

GLuint m_nVBOId;
glGenBuffers(1, &m_nVBOId);

使用函数 glBindBuffer 绑定这个缓存对象的类型。

glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);  

GL_ARRAY_BUFFER 表示数组缓存对象。

(2)为VBO设置顶点属性数据

使用函数 glBufferData 将顶点数据复制到缓冲内存中。
它的函数原型如下:
glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage)

  • 参数 target :表示buffer的类型,这里为 GL_ARRAY_BUFFER。
  • 参数 size : 表示传输数据的大小(以字节为单位)。
  • 参数 data : 数据指针。
  • 参数 usage :表示希望显卡如何管理数据。他有如下几种形式:
    • GL_STATIC_DRAW : 数据不会或几乎不会改变。
    • GL_DYNAMIC_DRAW :数据会被改变很多。
    • GL_STREAM_DRAW :数据每次绘制时都会改变。

如果数据不会改变,设置为 GL_STATIC_DRAW 就可以了, 如果设置为 GL_DYNAMIC_DRAW 或者 GL_STREAM_DRAW 显卡会把数据放在高速写入的内存。

这里我们的顶点数据定义如下:

struct VertexAttributeData
{
    // Postion
    float pos[3];
    float color[4];
};

 // 创建顶点属性数据
VertexAttributeData vAttrData[3];
createVertexAttributeData(vAttrData);

因此这里可以这么调用:

glBufferData(GL_ARRAY_BUFFER, sizeof(vAttrData), vAttrData, GL_STATIC_DRAW);
(3)链接顶点属性

函数 glVertexAttribPointer 用于告诉 OpenGL 如何解析顶点数据。
函数原型: glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer)

  • 参数 index :表示顶点属性的位置值。(后面Shader中会讲到)
  • 参数 size : 表示顶点属性的大小。(比如三角形的一个顶点是三个分量)
  • 参数 type :表示参数的数据类型。这里为 GL_FLOAT
  • 参数 normalized : 表示是否希望被标准化,如果设置为 GL_TRUE 则会被映射到 [0, 1] 之间。这里我们设置为 GL_FALSE
  • 参数 stride :表示步长。因为我这里使用结构体表示的,每个顶点数据的步长为 sizeof(VertexAttributeData)
  • 参数 pointer : 表示偏移。设置顶点数据时为 (void)0* ,设置颜色数据时为 (void)(sizeof (float) * 3)* 因为它的每个顶点属性的偏移为 sizeof (float) * 3(前面三个为顶点数据)

5. 编写着色器(Shader)

着色器 Shader 的代码与C语言很像。

(1) 顶点着色器(Vertex Shader)

Shader的完整代码如下:

attribute highp vec3 color;
attribute highp vec3 pos;

varying vec4 M_Color;

void main(void)
{
    gl_Position = vec4(pos, 1.0);
    M_Color = vec4(color, 1.0);
}

这里 pos 为顶点数据,color 为颜色数据, 他们均来在VBO。vec3 表示3个元素的向量, vec4 表示四个元素的向量。
gl_Position表示处理后的顶点信息。(处理后的标准设备坐标)

  • 这里的 attribute 表示属性数据,只能在VertexShader中使用。CPU与VertexShader之间传递数据。
  • varying : Shader之间传递数据,表示M_Color的值向下传递。

(2)片段着色器(Fragment Shader)
varying vec4 M_Color;

void main(void)
{
    gl_FragColor = M_Color;
}

Fragment Shader比较简单,gl_FragColor设置颜色信息。

(3)编译着色器

Qt中为我们提供了着色器相关的类: QOpenGLShaderProgramQOpenGLShader

这里的初始化代码如下:

bool OpenglRenderWidget::initShaderProgram(void)
{
    m_pShaderProgram = new QOpenGLShaderProgram(this);

    // 加载顶点着色器
    QString vertexShaderStr(":/vertexshader.vsh");
    m_pVertexShader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    bool result = m_pVertexShader->compileSourceFile(vertexShaderStr);
    if (!result)
    {
        qDebug() << m_pVertexShader->log();
        return false;
    }

    // 加载片段着色器
    QString fragmentShaderStr(":/fragmentshader.fsh");
    m_pFragmentShader = new QOpenGLShader(QOpenGLShader::Fragment, this);
    result = m_pFragmentShader->compileSourceFile(fragmentShaderStr);
    if (!result)
    {
        qDebug() << m_pFragmentShader->log();
        return false;
    }

    // 创建ShaderProgram
    m_pShaderProgram = new QOpenGLShaderProgram(this);
    m_pShaderProgram->addShader(m_pVertexShader);
    m_pShaderProgram->addShader(m_pFragmentShader);
    return m_pShaderProgram->link();
}

使用函数 compileSourceFile 编译Shader文件,如果出错可以使用函数 log 打印错误信息。


6. 绘制三角形

创建完 Shader程序后,可以获取位置和颜色的顶点属性位置值(Attribute Location)。 在函数 glVertexAttribPointer 设置顶点信息属性指针时使用。

下面是完整的初始化部分代码:

void OpenglRenderWidget::initializeGL()
{
    this->initializeOpenGLFunctions();

    // 初始化GPU程序
    bool result = initShaderProgram();
    if (!result)
        return;

    m_shaderProgramId = m_pShaderProgram->programId();
    // 获取位置和颜色的locationID
    m_nPosAttrLocationId = glGetAttribLocation(m_shaderProgramId, "pos");
    m_nColorAttrLocationId = glGetAttribLocation(m_shaderProgramId, "color");

    // 创建顶点属性数据
    VertexAttributeData vAttrData[3];
    createVertexAttributeData(vAttrData);

    // 创建VBO
    glGenBuffers(1, &m_nVBOId);

    // 初始化VBO
    glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vAttrData), vAttrData, GL_STATIC_DRAW);

    // 设置顶点信息属性指针
    glVertexAttribPointer(m_nPosAttrLocationId, 3, GL_FLOAT, GL_FALSE, sizeof(VertexAttributeData), (void*)0);
    glEnableVertexAttribArray(m_nPosAttrLocationId);
    // 设置原色信息属性指针
    glVertexAttribPointer(m_nColorAttrLocationId, 3, GL_FLOAT, GL_FALSE, sizeof(VertexAttributeData), (void*)(sizeof (float) * 3));
    glEnableVertexAttribArray(m_nColorAttrLocationId);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

这里使用函数 glDrawArrays 绘制这个三角形。

它的函数原型如下:

glDrawArrays(GLenum mode, GLint first, GLsizei count)
  • 参数 mode : 绘制的图元类型。GL_TRIANGLES 表示三角形
  • 参数 first : 顶点数组的起始索引。这里填0。
  • 参数 count : 表示绘制多少个顶点。这里填 3。

绘制部分代码如下:

void OpenglRenderWidget::paintGL()
{
    glClearColor(51.0f / 255.0f, 76.0f / 255.0f, 76.0f / 255.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 使用shader
    m_pShaderProgram->bind();
    
    // 绑定 VBO
    glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
    // 绘制
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    m_pShaderProgram->release();
}

显示效果:


完整代码如下:

头文件:

#ifndef OPENGLRENDERWIDGET_H
#define OPENGLRENDERWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions_2_0>
#include <QOpenGLFunctions_3_3_Core>

class OpenglRenderWidget : public QOpenGLWidget, public QOpenGLFunctions_2_0
{
    Q_OBJECT
public:
    struct VertexAttributeData
    {
        // Postion
        float pos[3];
        float color[3];
    };

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

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

private:
    bool initShaderProgram(void);
    void createVertexAttributeData(VertexAttributeData* pVetAttr);

    GLuint m_shaderProgramId;
    QOpenGLShaderProgram* m_pShaderProgram = nullptr;
    QOpenGLShader* m_pVertexShader = nullptr;
    QOpenGLShader* m_pFragmentShader = nullptr;

    GLuint m_nVBOId;

    // Attribute Location
    GLint m_nPosAttrLocationId;
    GLint m_nColorAttrLocationId;
};

#endif

CPP文件

#include "OpenglRenderWidget.h"
#include <QDebug>

OpenglRenderWidget::OpenglRenderWidget(QWidget* parent)
    :QOpenGLWidget(parent)
{

}

OpenglRenderWidget::~OpenglRenderWidget()
{

}

void OpenglRenderWidget::initializeGL()
{
    this->initializeOpenGLFunctions();

    // 初始化GPU程序
    bool result = initShaderProgram();
    if (!result)
        return;

    m_shaderProgramId = m_pShaderProgram->programId();
    // 获取位置和颜色的locationID
    m_nPosAttrLocationId = glGetAttribLocation(m_shaderProgramId, "pos");
    m_nColorAttrLocationId = glGetAttribLocation(m_shaderProgramId, "color");

    // 创建顶点属性数据
    VertexAttributeData vAttrData[3];
    createVertexAttributeData(vAttrData);

    // 创建VBO
    glGenBuffers(1, &m_nVBOId);

    // 初始化VBO
    glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vAttrData), vAttrData, GL_STATIC_DRAW);

    // 设置顶点信息属性指针
    glVertexAttribPointer(m_nPosAttrLocationId, 3, GL_FLOAT, GL_FALSE, sizeof(VertexAttributeData), (void*)0);
    glEnableVertexAttribArray(m_nPosAttrLocationId);
    // 设置原色信息属性指针
    glVertexAttribPointer(m_nColorAttrLocationId, 3, GL_FLOAT, GL_FALSE, sizeof(VertexAttributeData), (void*)(sizeof (float) * 3));
    glEnableVertexAttribArray(m_nColorAttrLocationId);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

void OpenglRenderWidget::resizeGL(int w, int h)
{
    this->glViewport(0, 0, w, h);

    return QOpenGLWidget::resizeGL(w, h);
}

void OpenglRenderWidget::paintGL()
{
    glClearColor(51.0f / 255.0f, 76.0f / 255.0f, 76.0f / 255.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 使用shader
    m_pShaderProgram->bind();

    glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
    // 绘制
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    m_pShaderProgram->release();
}

bool OpenglRenderWidget::initShaderProgram(void)
{
    m_pShaderProgram = new QOpenGLShaderProgram(this);

    // 加载顶点着色器
    QString vertexShaderStr(":/vertexshader.vsh");
    m_pVertexShader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    bool result = m_pVertexShader->compileSourceFile(vertexShaderStr);
    if (!result)
    {
        qDebug() << m_pVertexShader->log();
        return false;
    }

    // 加载片段着色器
    QString fragmentShaderStr(":/fragmentshader.fsh");
    m_pFragmentShader = new QOpenGLShader(QOpenGLShader::Fragment, this);
    result = m_pFragmentShader->compileSourceFile(fragmentShaderStr);
    if (!result)
    {
        qDebug() << m_pFragmentShader->log();
        return false;
    }

    // 创建ShaderProgram
    m_pShaderProgram = new QOpenGLShaderProgram(this);
    m_pShaderProgram->addShader(m_pVertexShader);
    m_pShaderProgram->addShader(m_pFragmentShader);
    return m_pShaderProgram->link();
}

void OpenglRenderWidget::createVertexAttributeData(VertexAttributeData* pVetAttr)
{
    // 第一个点位置信息
    pVetAttr[0].pos[0] = 0.0f;
    pVetAttr[0].pos[1] = 0.5f;
    pVetAttr[0].pos[2] = 0.0f;
    // 第一个点颜色信息
    pVetAttr[0].color[0] = 1.0f;
    pVetAttr[0].color[1] = 0.0f;
    pVetAttr[0].color[2] = 0.0f;

    // 第二个点位置信息
    pVetAttr[1].pos[0] = -0.5f;
    pVetAttr[1].pos[1] = -0.5f;
    pVetAttr[1].pos[2] = 0.0f;
    // 第二个点颜色信息
    pVetAttr[1].color[0] = 0.0f;
    pVetAttr[1].color[1] = 1.0f;
    pVetAttr[1].color[2] = 0.0f;

    // 第三个点位置信息
    pVetAttr[2].pos[0] = 0.5f;
    pVetAttr[2].pos[1] = -0.5f;
    pVetAttr[2].pos[2] = 0.0f;
    // 第三个点颜色信息
    pVetAttr[2].color[0] = 0.0f;
    pVetAttr[2].color[1] = 0.0f;
    pVetAttr[2].color[2] = 1.0f;
}
不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:Qt与OpenGL编程 - IBO与线框模式