3. Qt OpenGL

OpenGL(Open Graphics Library,开源图形库)是用于渲染2D和3D计算机图形的广泛应用的行业标准。 它是Mac OS X、Linux和大多数嵌入式平台上硬件加速图形操作的标准。

OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。 至于内部具体每个函数是如何实现的,将由编写OpenGL库的人自行决定,实际通常是GPU的生产商。

3.1. 鲁班猫RK系列板卡

鲁班猫RK系列的lubancatZ/W和lubancat1/2板卡,是使用瑞芯微的rk356x处理器,3D图像渲染的支持:

qtled001.png

lubancat-4板卡,是使用瑞芯微的rk3588S处理器,3D图像渲染的支持:

qtled001.png

rk356x处理器GPU是Mail-G52 GPU,rk3588X处理器GPU是Mali-G610,都支持OpenGL ES 1.1,2.0和3.2。

OpenGL ES(OpenGL for Embedded Systems)是OpenGL规范的一种形式,适用于嵌入式设备。 OpenGL ES 1.x版本、2.x 版本和 3.x 版本均可提供高性能图形界面,用于创建3D、可视化图表和界面等。

其中1.x版本支持固定管线(立即渲染模式)等,2.x版本支持可编程管线等,3.x支持mrt、纹理压缩、compute shader等, 2.x和3.x的图形编程基本相似,不同之处在于版本3.x表示2.x与其他功能的超集。

下面测试主要使用OpenGL ES 3.2(或者使用OpenGL ES 2.0)。

3.2. Qt中OpenGL相关类

Qt GUI 核心模块集成了OpenGL与OpenGL ES接口。

3.2.1. QWindow

Qt GUI中最重要的类是QGuiApplication和 QWindow ,Qt应用程序将需要利用这两个类将内容显示在屏幕上。 其中 QWindow 类表示在底层窗口系统的窗口,它提供了许多虚拟功能来处理来自窗口系统的事件(QEvent), 例如触摸输入,曝光,焦点,按键和几何形状更改。

3.2.2. QGLWidget

QGLWidget 类是用于渲染OpenGL图形的窗口, 提供了显示集成到Qt应用程序中的OpenGL图形的功能。使用方式是通过子类继承,就像其他任何QWidget一样,可以选择使用QPainter或者标准OpenGL渲染命令。

注意:这个类是传统QtOpenGL模块的一部分,与其他Qt OpenGL类一样,应该在新的应用程序中避免使用。从Qt5.4开始,Qt推荐使用QOpenGLWidget和QOpenGL类。

3.2.3. QOpenGLWidget

QOpenGLWidget 是Qt库提供的一个类,提供了显示集成到Qt应用程序中的OpenGL图形的功能。

它是QWidget类的子类,提供了基本的显示OpenGL图形功能和一些额外的功能, 比如用于与OpenGL上下文交互,例如基于事件的机制处理键盘和鼠标输入,以及在多个窗口部件之间共享OpenGL资源的能力。

使用QOpenGLWidget的主要优点是它允许您轻松地将OpenGL图形集成到基于Qt的应用程序中,并提供了一种简单一致的方式来处理OpenGL上下文和事件。 QOpenGLWidget是为了更现代,更高效地替代QGLWidget而设计的,并且推荐用于新的开发。它支持例如使用现代OpenGL核心配置文件和在多线程中使用OpenGL上下文等功能。

QOpenGLWidget的使用非常简单:创建子类类继承它,并像任何其他QWidget一样使用子类,可以在使用QPainter和标准OpenGL渲染命令之间进行选择。

QOpenGLWidget提供了三个方便的虚拟函数,可以方便的在子类中重新实现这些函数来执行OpenGL:

1、initializeGL()执行OpenGL资源初始化,在第一次调用resizeGL()或paintGL()之前调用一次;

2、resizeGL()用于设置转换矩阵和其他依赖于窗口大小的资源等,每当调整Widget的大小时调用(或者窗口自动获得调整大小事件而首次显示时);

3、paintGL()渲染OpenGL场景使用QPaint绘制,更新窗口时就会调用。

新的应用中,一般建议使用QOpenGLWidge。

3.2.4. QOpenGLWindow

QOpenGLWindow 是Qt中用来提供更底层的OpenGL渲染支持的类, 使用与QOpenGLWidget兼容的API轻松创建执行OpenGL渲染的窗口,并且类似于传统的QGLWidget, 与QOpenGLWidget不同,QOpenGLWindow不依赖于widgets模块,并且提供了更好的性能。

QOpenGLWindow是 QWindow 的子类,扩展了 QWindow的功能,提供了基本的OpenGL渲染功能。

QOpenGLWindow提供了一组虚拟函数,用于在窗口渲染时调用,如initializeGL()和paintGL();可以在这些函数中使用OpenGL函数来绘制图形; 也提供了一些额外的功能,用于在多线程中使用OpenGL上下文,并支持使用现代OpenGL特性。

和QOpenGLWidget使用方式一样,继承QOpenGLWindow,并重新实现以下虚拟功能:

1、initializeGL()执行OpenGL资源初始化;

2、resizeGL()来设置转换矩阵和其他依赖于窗口大小的资源;

3、paintGL()发出OpenGL命令或使用QPainter绘制;

更多描述参考下https://doc.qt.io/qt-5/qopenglwindow.html。

3.2.5. QOpenGLFunctions

QOpenGLFunctions 类提供对 OpenGL ES 2.0 API的跨平台访问。

使用QOpenGLFunctions的推荐方法是直接继承,同时在初始化函数中void initializeGL() 调用此接口initializeOpenGLFunctions() 进行初始化。

3.2.6. QOpenGLExtraFunctions

QOpenGLExtraFunctions 类提供对OpenGL ES 3.0、3.1和3.2 API的跨平台访问,继承自QOpenGLFunctions。

还有更多的相关类参考下:https://doc.qt.io/qt-5/qtgui-index.html#opengl-and-opengl-es-integration

3.3. QT中使用OpenGL

Qt对OpenGL有很好的支持,无论固定管线版本,还是可编程管线版本。 可以使用适用于Qt自身的QOpenGLShader与QOpenGLShaderProgram等类,或者使用忠实于原头文件glfw.h的QOpenGLFunction_x_x_Core类。

QOpenGLFunction_x_x_Core类提供了对应OpenGL版本和配置文件的所有函数包装。例如QOpenGLFunctions_3_2_Core, 提供OpenGL 3.2核心配置文件的所有功能。

3.3.1. 创建一个窗口

下面我们将在Qt中简单使用OpenGL ES,并在鲁班猫板卡上上运行, 先创建一个工程,基于QMainWindow,工程名称为MyTriangle。 然后往该项目中添加一个Triangle类,继承于QOpenGLWidget和QOpenGLFunctions:

lubancat_qt_tutorial_code/Opengl/Triangle/triangle.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Triangle : public QOpenGLWidget, protected QOpenGLFunctions
{
public:
    explicit  Triangle(QWidget *parent = nullptr);
    ~Triangle();

protected:
    // 继承QOpenGLWidget后重写这三个虚函数
    virtual void initializeGL() override;
    virtual void resizeGL(int w, int h) override;
    virtual void paintGL() override;
};

在Triangle类中,将重新实现initializeGL()、paintGL()和resizeGL()函数,具体如下:

lubancat_qt_tutorial_code/Opengl/Triangle/triangle.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "triangle.h"

Triangle::Triangle(QWidget *parent) : QOpenGLWidget(parent)
{
}

Triangle::~Triangle(){
}

void Triangle::initializeGL()
{
    // 初始化OpenGL函数,如果继承QOpenGlFunctions,必须使用这个初始化函数
    initializeOpenGLFunctions();
}

void Triangle::resizeGL(int w, int h)
{
    // OpenGL渲染窗口的尺寸大小,glViewport可以设置位置和宽高
    glViewport(0, 0, w, h);
}

void Triangle::paintGL()
{

    // 设置清屏颜色
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    // 清空屏幕的颜色缓冲区,填充glClearColor设置的颜色
    glClear(GL_COLOR_BUFFER_BIT);
}

Qt Designer中,在主窗口添加一个OOpenGL Widget窗口,右击窗口该窗口,点击 提升为... , 添加前面的Triangle类,提升为Triangle:

qtled001.png

在MainWindow.cpp中将新创建的openGLWidget设置为centralWidget,编译,然后复制可执行程序到鲁班猫板运行(或者远程连接部署):

qtled001.png

接下来我们将绘制一个三角形,这需要很多了解很多OpenGL概念以及图形学相关知识,可以参考 https://learnopengl-cn.github.io

3.3.2. 绘制一个三角形

在OpenGL中,可以使用三维空间中的坐标定义绘制的对象,要绘制一个三角形,必须先定义其坐标,OpenGL中一般是定义浮点数的顶点数组坐标,然后存储在一个缓冲区中。

OpenGL中所有的事物都是在3D空间中,但屏幕和窗口时2D像素数组,这导致OpenGL的大部分工作是把3D坐标转变为适应你屏幕的2D像素, 而3D坐标转为2D坐标的处理过程是由OpenGL的 图形渲染管线 (Graphics Pipeline)管理。

GPU渲染管线部分多个阶段,可以参考下 这里

图形渲染管线的各个阶段分为可编程的模块,可配置的部分,固定的部分。 在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。

  • 顶点着色器 主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。

  • 片段着色器 的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。 通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

示例程序如下:

lubancat_qt_tutorial_code/Opengl/Triangle/triangle.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
static const char *vertexShaderSource =
    "#version 320 es\n"
    "layout (location = 0) in vec4 posAttr;\n"
    "layout (location = 1) in vec3 colAttr;\n"
    "uniform mat4  matrix;\n"
    "out lowp vec3 col;\n"
    "void main() {\n"
    "   col = colAttr;\n"
    "   gl_Position = matrix * posAttr;\n"
    "}\n";

static const char *fragmentShaderSource =
    "#version 320 es\n"
    "in highp vec3 col;\n"
    "out highp vec4 FragColor;\n"
    "void main() {\n"
    "   FragColor = vec4(col, 1.0);\n"
    "}\n";

void Triangle::initializeGL()
{
    // 初始化OpenGL函数,如果继承QOpenGlFunctions,必须使用这个初始化函数
    initializeOpenGLFunctions();

    //着色器
    m_program = new QOpenGLShaderProgram(this);
    m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
    m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);

    //链接
    if(!m_program->link()){
        qDebug() << "link failed!\n";
    }

    // 使用attributeLocation函数来获取xxx属性的位置
    m_posAttr = m_program->attributeLocation("posAttr");   //顶点属性
    m_colAttr = m_program->attributeLocation("colAttr");   //颜色属性
    m_matrixUniform = m_program->uniformLocation("matrix");  //全局变量

    m_program->bind();

    //三个顶点
    static const GLfloat vertices[] = {
        0.0f,  0.707f,
        -0.5f, -0.5f,
        0.5f, -0.5f
    };

    //颜色,在OpenGL或GLSL中强制归一化到[0.0,1.0]之间的
    static const GLfloat colors[] = {
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f
    };

    //链接顶点、颜色属性,告诉OpenGL该如何解析顶点数据,颜色属性
    glVertexAttribPointer(m_posAttr, 2, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer(m_colAttr, 3, GL_FLOAT, GL_FALSE, 0, colors);

    //启用顶点、颜色属性
    glEnableVertexAttribArray(m_posAttr);
    glEnableVertexAttribArray(m_colAttr);
}

void Triangle::resizeGL(int w, int h)
{
    // OpenGL渲染窗口的尺寸大小,glViewport可以设置位置和宽高
//    glViewport(0, 0, w, h);
    Q_UNUSED(w);
    Q_UNUSED(h);

    const qreal retinaScale = devicePixelRatio();
    glViewport(0, 0, width() * retinaScale, height() * retinaScale); //视口的宽度和高度将根据设备的像素比例进行缩放

}

void Triangle::paintGL()
{

    // 设置清屏颜色
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    // 清空屏幕的颜色缓冲区,填充glClearColor设置的颜色
    glClear(GL_COLOR_BUFFER_BIT);

    m_program->bind();

    QMatrix4x4 matrix;    //4x4的单位矩阵
    matrix.perspective(60.0f, 4.0f / 3.0f, 0.1f, 100.0f);                //透视矩阵变换
    matrix.translate(0, 0, -2);                                          //平移变换
    matrix.rotate(100.0f * m_frame / screen()->refreshRate(), 0, 1, 0);  //旋转

    m_program->setUniformValue(m_matrixUniform, matrix);

    glDrawArrays(GL_TRIANGLES, 0, 3);                           //传递图元,绘制三角

    //关闭启用顶点、颜色数组等
    //......
    ++m_frame;
}

整体的程序如上,我们创建的Triangle类是继承QOpenGLFunctions,使用OpenGL ES 2.0 API,例程中使用Qt的封装的OpenGL接口(例程主函数中设置使用3.2版本,向后兼容2.0)。

我们重新实现initializeGL虚函数,OpenGL资源和状态。 先initializeOpenGLFunctions()函数为当前上下文初始化opengl函数解析,然后创建着色器对象(一个片段着色器和一个顶点着色器), 着色器是着色器语言GLSL(OpenGL Shading Language)编写,并暂时以字符串的形式进行保存:

lubancat_qt_tutorial_code/Opengl/Triangle/triangle.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 顶点着色器
#version 320 es     //版本号
layout (location = 0) in vec4 posAttr;   //输入变量名为posAttr,位置属性0
layout (location = 1) in vec3 colAttr;   //输入变量名为colAttr,颜色属性,位置属性1
uniform mat4  matrix;     //uniform 全局变量,一个四维矩阵,用来变换
out lowp vec3 col;
void main() {
   col = colAttr;                   //传递颜色属性
   gl_Position = matrix * posAttr;  //gl_Position内置关键字,为顶点着色器的输出的顶点位置数据
};
lubancat_qt_tutorial_code/Opengl/Triangle/triangle.cpp
1
2
3
4
5
6
7
// 片段着色器
#version 320 es    //版本号
in highp vec3 col;          //输入变量,从顶点着色器传来的输入变量
out highp vec4 FragColor;
void main() {
   FragColor = vec4(col, 1.0);  //输出变量
};

片段着色器需要生成一个最终输出的颜色,可以通过从一个着色器向另一个着色器发送数据,这必须在发送方着色器中声明一个输出(即前面顶点着色器的col变量), 在接收方着色器中声明一个类似的输入(即前面片段着色器的输入变量col)。当类型和名字都一样的时候, OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。

在resizeGL()中,设置OpenGL渲染窗口的尺寸大小,即视口(Viewport)的大小。

最后实现旋转效果,需要添加一个QTimer,定时更新窗口:

lubancat_qt_tutorial_code/Opengl/Triangle/triangle.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Triangle::Triangle(QWidget *parent) : QOpenGLWidget(parent)
{
    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, &Triangle::animation);
    timer->start(20);
}

void Triangle::animation()
{
    update();
}

编译,然后在鲁班猫上测试,就会绘制一个彩色三角形,并且在绕y轴旋转:

qtled001.png

以上就是一个简单的示例,更多的参考下Qt帮助手册和OpenGL API参考手册。