OpenGL是一个跨平台的、用来渲染3D图形的标准API,Qt对OpenGL提供了强大的支持。Qt4时代的QtOpenGL模块在Qt5中已经不再建议使用,OpenGL相关的类被移到了Qt GUI模块。Qt Widgets模块中的QOpenGLWidget类提供了一个可以渲染OpenGL图形的部件,通过该部件可以轻松地将OpenGL图形整合到Qt应用程序中。

本章不会对OpenGL的专业知识进行过多讲解,只会涉及在Qt应用程序中进行
3D绘图的一.些最基本应用。如果想深入学习,则可以参考Qt GUI模块帮助文档中
OpenGL and OpenGL ES Integration 部分内容。


使用OpenGL绘制图形介绍

QOpenGLWidget类是一个用来渲染OpenGL图形的部件,它提供了在Qt应用程序中显示OpenGL图形的功能。

这个类使用起来很简单,只需要继承该类,然后像使用其他QWidget部件一样来使用它即可。

QGLWidget 提供了3个方便的虚函数,可以在子类中重新实现它们来执行典型的OpenGL任务:

  • initializeGL():设置OpenGL资源和状态。
    该函数只在第一次调用resizeGL()或paintGL()前被调用一次;

  • resizeGL():设置OpenGL的视口、投影等。
    每次部件改变大小时都会调用该函数;

  • paintGL():渲染OpenGL场景。
    每当部件需要更新时都会调用该函数。

着色器

从OpenGL2.0开始引人着色器的概念,除了固定功能的管线以外,增加了一种可编程着色管线,可以通过着色器控制顶点和片段的处理

从OpenGL 3.1开始,固定功能的管线被废弃并删除了,于是必须使用着色器来完成工作。

着色器是使用OpenGL着色语言(OpenGL Shading Language,GLSL)编写的一个小型函数
绘图时需要至少指定两个着色器:顶点着色器(vertexshader)片段着色器( fragmentshader,也称为片元着色器)。

Qt中QOpenGLShader类用来创建和编译着色器,支持使用OpenGL着色语言GLSLOpenGL/ES着色语言GLSL/ES编写的着色器。

QOpenGL ShaderProgram类用来创建并设置着色器程序,可以链接多个着色器,并在OpenGL当前环境(current context,也称为当前上下文)中绑定着色器程序。

QOpenGLFunctions 类提供了对OpenGL ES2.0 API的访问接口,QOpenGLExtraFunctions提供了对OpenGL ES3.0和3.1API的访问接口。

QAbstractOpenGLFunctions 是一个类族的基类,类族中的类涉及了所有OpenGL版本. 并为相应版本OpenGL的所有函数提供了访问接口。
例如,QOpenGLFunctions_ 2_1,QOpenGLFunctions_4_3_Core和QOpenGLFunetions_4_3_Compatibilit等

相关内容可以查看QAbstractOpenGLFunctions类的帮助文档。


例子 QOpenGLWidget中使用OpenGL绘制图形

下面通过一个简单的例程来看一下怎样在QOpenGLWidget中使用OpenGL绘制图形

读者也可以参考Qt中的Hello GL2 Example示例程序。

(项目源码:myopengl)新建空的Qt项目Empty qmake Project,项目名称为myopengl.完成后往项目中添加新的C++类,类名为MyOpenGLWidget,基类先不进行设置。

下面打开项目文件myopengl. pro,添加一行代码:

QT += widgets

保存该文件。再打开myglwidget. h文件,更改如下:

#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
class QOpenGLShaderProgram;

class MyOpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
    Q_OBJECT

public:
    explicit MyOpenGLWidget(QWidget *parent = 0);

protected:
    void initializeGL();
    void paintGL();
    void resizeGL(int width, int height);

private:
    QOpenGLShaderProgram *program;
};
#endif // MYOPENGLWIDGET_H
class MyOpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions

这里使用了多继承,自定义的MyOpenGLWidget类同时继承自QOpenGLWidget
类和QOpenGLFunctions类,这样就可以在类中直接使用QOpenGLFunctions中的
OpenGL函数,而不需要创建QOpenGLFunctions 对象

  QOpenGLShaderProgram *program;

这里声明了一个QOpenGLShaderProgram对象指针,作为着色器程序

下面到myglwidget. cpp文件中进行更改:
先添加头文件#include < QOpenGLShaderProgram>并更改构造函数:

#include "myopenglwidget.h"
#include <QOpenGLShaderProgram>

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

然后添加initializeGL()函数定义:

void MyOpenGLWidget::initializeGL()
{
    // 为当前环境初始化OpenGL函数
    initializeOpenGLFunctions();

    // 创建顶点着色器
    QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    const char *vsrc =
            "void main() {                             \n"
            "   gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n"
            "}                                         \n";
    vshader->compileSourceCode(vsrc);
    // 创建片段着色器
    QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
    const char *fsrc =
            "void main() {                              \n"
            "   gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n"
            "}                                          \n";
    fshader->compileSourceCode(fsrc);

    // 创建着色器程序
    program = new QOpenGLShaderProgram;
    program->addShader(vshader);
    program->addShader(fshader);
    program->link();
    program->bind();
}

这里解释一下:

// 为当前环境初始化OpenGL函数
    initializeOpenGLFunctions();

这里首先调用QOpenGLFunctions::initializeOpenGLFunctions( )对OpenGL函数进行了初始化,这样QOpenGLFunctions中的函数只能在当前环境中使用。

   // 创建顶点着色器
    QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    //设置源码并编译
    const char *vsrc =
            "void main() {                             \n"
            "   gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n"
            "}                                         \n";
    vshader->compileSourceCode(vsrc);
    // 创建片段着色器
    QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
     //设置源码并编译
    const char *fsrc =
            "void main() {                              \n"
            "   gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n"
            "}                                          \n";
    fshader->compileSourceCode(fsrc);

然后进行了着色器的相关设置。使用QOpenGLShader创建了一个顶点着色器和一个片段着色器,并使用compileSourceCode()函数为着色器设置了源码并进行了编译。

   // 创建着色器程序
    program = new QOpenGLShaderProgram;
    program->addShader(vshader);
    program->addShader(fshader);
    program->link();
    program->bind();

下面创建了着色器程序QOpenGLShaderProgram对象,使用addShader( )将前面已经编译好的着色器添加进来,然后调用link()函数将所有加人到程序中的着色器链接到一起,最后
调用bind()函数将该着色器程序绑定到当前OpenGL环境中。

为了使程序尽量简单,这里直接在程序中编写了着色器源码;

对于较复杂的着色器源码,一般是写在文件中的,可以使用compileSourceFile( )进行加载编译。


这个程序只是绘制一个白色的点,所以只需要指定一个顶点(gl_Position)vec4 (0.0, 0.0,0.0, 1. 0)和渲染颜色(gl_FragColor)vec4(1.0, 1.0,1.0,1.0),这里的vec4类型GLSL的4位浮点数向量.


这里对基本内容进行简单介绍:

可以把整个窗口的中心当作坐标原点,X轴从左到右,Y轴从下到上,Z轴从里到外,顶点(0.0, 0.0,0.0,1.0)的前3个分量分别是X.Y和Z轴的坐标,第4个分量默认为1.0,一般不用设置,所以该顶点就是坐标原点,后面会显示到窗口的中心。
在这里插入图片描述

颜色(gl_FragColor)(1.0,1.0, 1.0, 1.0)的前3个分量分别对应R红色、G绿色和B蓝色,第4个分量A对应alpha值,用于设置透明度,因为RGB均设置为1.0, 所以为白色。透明度1为不透明。


下面添加resize设置大小的函数:

void MyOpenGLWidget::resizeGL(int , int )
{
}

这里现在先保留为空,该函数不是必须设置的。


继续添加paintGL()绘制函数的定义:

void MyOpenGLWidget::paintGL()
{
    // 绘制
    glDrawArrays(GL_POINTS, 0, 1);
}

作为简单示例,这里直接调用了glDrawArrays()函数来进行OpenGL图形绘制。

glDrawArrays()函数原型为:

void QOpenGLFunctions::glDrawArrays(GLenum mode, GLint first, GLsizei count)

该函数使用当前绑定的顶点数组元素来建立几何图形.

第1个参数mode设置了构建图形的类型,如

  • GL_POINTS(点)
  • GL_LINES(线)
  • GL_LINE_STRIP(条带线)
  • GL_LINE_LOOP(循环线)
  • GL_TRIANGLES(独立三角形)
  • GL_TRIANGLE_STRIP(三角形条带)
  • GL_TRIANGLE_FAN(三角形扇面)等;

第2个参数first 指定元素起始位置

第3个参数count元素个数

就是用顶点数组中索引为first~first+count-1的元素为顶点来绘制mode指定的图形。

这里是glDrawArrays(GL_POINTS, 0, 1);绘制点,开始为0原点,个数为1

最后再向项目中添加 main.cpp文件,更改内容如下:

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

int main(int argc, char *argv[])
{
    QApplication app(argc,argv);
    MyOpenGLWidget w;
    w.resize(400, 300);
    w.show();
    return app.exec();
}

创建了一个 MyOpenGLWidget w;

现在运行程序可以看到,窗口背景默认为黑色,窗口中间绘制了一个白色点。
在这里插入图片描述

虽然很小,但是有的

这个程序虽然简单,但是展示了在Qt中绘制OpenGL图形的一般过程。下面将在这个程序的基础上进一步讲解OpenGL的其他内容。


绘制多边形

前面是一个顶点,绘制多边形需要更多的顶点

设置顶点一般使用数组来实现,然后将数组中的顶点数据输入到顶点着色器中。为了获得更好的性能,一般还会使用缓存。

使用顶点数组

(项目源码: src\12\12- 2\myopengl)继续在前面的程序中进行更改。首先将顶点着色器源码更改如下:

    // 创建顶点着色器
    QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    const char *vsrc =
            "in vec4 vPosition;                        \n"
            "void main() {                             \n"
            "   gl_Position = vPosition;               \n"
            "}                                         \n";
    vshader->compileSourceCode(vsrc);

这里为顶点着色器声明了一个名为vPosition输入变量, in存储限制符表明了数据进入着色器的流向,与其对应的是out存储限制符,vPosition用于获取外部输入的顶点数据


//表明将输入的顶点位置复制到顶点着色器的指定输出位置gl_Position中。
“gl_Position = vPosition”

表明将输入的顶点位置复制到顶点着色器的指定输出位置gl_Position中。


下面来更改paintGL()绘画函数:

void MyOpenGLWidget::paintGL()
{
 // 顶点位置
    GLfloat vertices[] = {
        -0.8f, 0.8f,
        -0.8f, -0.8f,
        0.8f, -0.8f,
        0.8f, 0.8f
    };
     GLuint vPosition = program->attributeLocation("vPosition");
    glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, vertices);
    glEnableVertexAttribArray(vPosition);
    // 绘制
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
}

这里定义了一个顶点数组vertices[],一共4行,每行定义一个顶点位置。

在前面的例子中已经看到,顶点位置是vec4类型的,应该有4个值,但是这里每行只有2个值,其实vec4的默认值为(0,0,0,1);仅当指定了X和Y坐标时,其他两个坐标值将被自动指定为0和1。

这里以原点为中心设置了一个正方形的4个顶点,首先是左上角的顶点,然后沿逆时针方向设置了其他3个顶点,顶点顺序可以是顺时针也可以是逆时针,逆时针绘制出来的是正面,而顺时针绘制出来的是反面

 GLuint vPosition = program->attributeLocation("vPosition");

attributeLocation()可以返回变量在着色器程序参数列表中的位置,这里获取了vPosition的位置。

    glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, vertices);
    glEnableVertexAttribArray(vPosition);

然后使用glVertexAttribPointer()vPosition顶点数组vertices进行关联。

glVertexAttribPointer()原型如下:

inline void QOpenGLFunctions::glVertexAttribPointer(GLuint index, 
GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer)

该函数设置着色器中变量索引为index的变量对应的数据值。

其中:

  • index参数就是要输入变量的位置索引;
  • size表示每个顶点需要更新的分量数目,例如,这里vertices每行只有2个值,所以size为 2 ;
  • type指定了数组中元素的类型,例如,这里vertices是GLfloat类型的,所以这里type为GLfloat ;
  • normalized设置顶点数据在存储前是否需要进行归一化,这里设置为否;
  • stride是数组中每两个元素之间的大小偏移值,一般设置为0即可;
  • pointer 设置顶点数组指针或者缓存内的偏移量,这里使用了顶点数组,所以直接设置为vertices即可。
  • 最后需要使用glEnableVertexAttribArray()来启用顶点数组,这样就完成了所有设置。

调用glDrawArrays()进行绘制时需要设置图形类型为GL_TRIANGLE_FAN,因为有4个顶点,所以第3个参数为4。

这时运行程序可能发现,绘制的图形是较宽的长方形,而不是一个正方形,这是因为整个窗口的宽为400,高为300,


下面来解决这个问题。

在 paintGL()函数的开始添加如下代码:

    int w = width();
    int h = height();
    int side = qMin(w, h);
    glViewport((w - side) / 2, (h - side) / 2, side, side);

这里使用glViewport()设置视口为整个窗口中尽可能大的正方形,视口定义了所有OpenGL渲染操作最终显示的2D矩形。

(注意,这个操作本应该放到resizeGL()函数中进行,只是由于Qt版本原因,现在并不能实现应有的效果,所以放到了paintGl()中。)

paintGL()函数中一般还会调用glClear()来清除屏幕,在调用glViewport()之后添加如下一行代码:

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

这里清除了颜色缓存和深度缓存。

现在运行程序,并改变窗口大小,可以看到,正方形会随着窗口改变大小,但是不会被压缩变形,效果如图所示。
在这里插入图片描述

图—绘制正方形效果

使用缓存

前面程序使用的顶点数组中指定的数据会保存在客户端内存中,在进行glDrawArrays()等绘图调用时,这些数据必须从客户内存复制到图形内存

为了避免每次绘图时都复制这些数据,可以将其缓存到图形内存中

缓存对象在OpenGL服务器中创建,这样当需要顶点,索引,纹理图像等数据时,客户端程序就不需要每次都进行上传。

Qt中的QOpenGLBuffer类用来创建并管理OpenGL缓存对象

下面通过例子讲解该类的使用。
(项目源码:myopengl)继续在前面的程序中进行更改。先在myopenglwidget.h文件中添加头文件包含:

#include <QOpenGLBuffer>

然后添加private变量:

private:
    QOpenGLBuffer vbo;

下面到myopenglwidget.cpp中,在 paintGL()函数创建vertices数组后面添加如下代码:

 // 顶点位置
    GLfloat vertices[] = {
        -0.8f, 0.8f,
        -0.8f, -0.8f,
        0.8f, -0.8f,
        0.8f, 0.8f
    };

    vbo.create();
    vbo.bind();
    vbo.allocate(vertices, 20*sizeof(GLfloat));
  1. 首先调用create()函数在OpenGL服务器中创建缓存对象
  2. 然后使用bind()函数将与该对象相关联的缓存绑定到当前OpenGL环境
  3. allocate()函数在缓存中为数组分配空间并将缓存初始化为数组的内容

创建好缓存以后﹐就可以通过缓存为顶点着色器输入数据了。

下面将paintGL()函数中调用的glVertexAttribPointer()函数替换为:

     program->setAttributeBuffer(vPosition, GL_FLOAT, 0, 2, 0);

这里:

 // 顶点位置
    GLfloat vertices[] = {
        -0.8f, 0.8f,
        -0.8f, -0.8f,
        0.8f, -0.8f,
        0.8f, 0.8f
    };

    vbo.create();
    vbo.bind();
    vbo.allocate(vertices, 8*sizeof(GLfloat));

    GLuint vPosition = program->attributeLocation("vPosition");
    program->setAttributeBuffer(vPosition, GL_FLOAT, 0, 2, 0);
    glEnableVertexAttribArray(vPosition);
//     glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, vertices);
//     glEnableVertexAttribArray(vPosition);

setAttributeBuffer()函数glVertexAttribPointer()函数类似,其函数原型如下:

void QOpenGLShaderProgram::setAttributeBuffer(int location, GLenum type, int offset, 
int tupleSize, int stride = 0)

该函数用来为着色器中 location位置的变量设置顶点缓存,offset指定了缓存中要使用数据的偏移值
通过调用该函数就可以将vPosition变量与缓存中的顶点数据进行关联


现在可以运行程序查看效果:

在这里插入图片描述

绘制彩色3D图形

1 为图形设置顶点颜色

项目源码myopengl ,继续在前面的代码上更改,首先要更改着色器源码:

 // 创建顶点着色器
    QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    const char *vsrc =
            "in vec4 vPosition;                        \n"
            "in vec4 vColor;                           \n"
            "out vec4 color;                           \n"
            "void main() {                             \n"
            "   color = vColor;                        \n"
            "   gl_Position = vPosition;               \n"
            "}                                         \n";
    vshader->compileSourceCode(vsrc);

这里声明了输入变量vColor输出变量color,并将vColor获取的颜色数据传递给color

输出变量可以将数据传递给后续阶段使用,这里主要是传递给片段着色器

下面更改片段着色器源码如下:

 // 创建片段着色器
    QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
    const char *fsrc =
            "in vec4 color;                             \n"
            "out vec4 fColor;                           \n"
            "void main() {                              \n"
            "   fColor = color;                         \n"
            "}                                          \n";
    fshader->compileSourceCode(fsrc);

这里声明了一个输入变量color,用来和顶点着色器的输出变量color对应。
输出变量fColor可以将color输入的颜色数据输出到着色管线中用来为图形着色。

下面到 paintGL()函数中,在 glDrawArrays()函数调用之前添加如下代码:

  // 顶点颜色
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f,
        1.0f, 1.0f, 1.0f
    };
    vbo.write(8*sizeof(GLfloat), colors, 12*sizeof(GLfloat));
    GLuint vColor = program->attributeLocation("vColor");
    program->setAttributeBuffer(vColor, GL_FLOAT, 8*sizeof(GLfloat), 3, 0);
    glEnableVertexAttribArray(vColor);

这里创建了一个颜色数组,共4行,分别为4个顶点进行着色

为了简便,这里直接在前面创建的缓存中写入了颜色数组数据,并为vColor变量指定了缓存。

write()函数原型如下:

void QOpenGLBuffer::write(int offset, const void *data, int count)

该函数会替换掉缓存中已有的内容,

  • 参数offset是要替换数据开始位置的偏移值,因为前面已经添加的顶点数组的大小为8·sizeof(GLfloat) ,所以这里需要将这个值作为偏移值。

为了不覆盖已有的数据,这里需要对缓存进行扩容,将前面程序中allocate()函数调用更改如下:

vbo.create();
    vbo.bind();
    vbo.allocate(vertices, 20*sizeof(GLfloat));

因为顶点数组有8个元素,颜色数组有12个元素,所以这里的大小设置为20·sizeof(GLfloat)。

现在运行程序可以看到,正方形的4个角分别是红,绿、蓝和白色。


在这里插入图片描述

这里我出现错误:not link,然后全黑
这里是OpenGL未调用。shader program is not linked

尝试安装directX 或者
禁用你的独立显卡,使用集成显卡

模拟器提示显卡/驱动版本较低,不支持dx11.0或OpenGL4.3以上怎么办?

QT5.12 Ui界面开发项目:QOpenGLShaderProgram::uniformLocation(model): shader program is not linked

2.实现3D效果

本小节采用的项目源码myopengl。继续在前面的程序中进行更改。首先更改顶点数组如下:

  // 顶点位置
    GLfloat vertices[2][4][3] = {
        { {-0.8f, 0.8f, 0.8f}, {-0.8f, -0.8f, 0.8f}, {0.8f, -0.8f, 0.8f}, {0.8f, 0.8f, 0.8f} },
        { {0.8f, 0.8f, 0.8f}, {0.8f, -0.8f, 0.8f}, {0.8f, -0.8f, -0.8f}, {0.8f, 0.8f, -0.8f} }
    };

    vbo.create();
    vbo.bind();
    vbo.allocate(vertices, 48*sizeof(GLfloat));

该数组每行指定了4个顶点(即一个正方形面),每个顶点由3个元素组成,因为要设置3D效果,所以每个顶点都指定了Z轴坐标

然后更改allocate()调用如下:

  vbo.allocate(vertices, 48*sizeof(GLfloat));

这里顶点数组有24个元素,后面颜色数组对应的也有24个元素,所以缓存大小为48sizeof(GLfloat)


下面更改设置vPosition的 setAttributeBuffer()函数:

 GLuint vPosition = program->attributeLocation("vPosition");
    program->setAttributeBuffer(vPosition, GL_FLOAT, 0, 3, 0);
    glEnableVertexAttribArray(vPosition);

因为现在数组中每个顶点由3个元素指定,所以这里第4个参数设置为3

下面更改颜色数组如下:

// 顶点颜色
    GLfloat colors[2][4][3] = {
        { {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f, 1.0f} },
        { {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f, 1.0f} }
    };

最后更改write函数调用:

 vbo.write(24*sizeof(GLfloat), colors, 24*sizeof(GLfloat));

这里24是24个元素(颜色数组)

下面更改VColor的setAttributeBuffer函数:

    program->setAttributeBuffer(vColor, GL_FLOAT, 24*sizeof(GLfloat), 3, 0);

最后绘制函数更改:

 // 绘制
    for(int i=0; i<2; i++)
        glDrawArrays(GL_TRIANGLE_FAN, i*4, 4);

这里要绘制两个面,所以用for()函数调用了2次 gIDrawArrays()函数进行绘制.

i*4: 第一次绘制用去了4个顶点,所以第2次调用时设置了起始位置(即第2个参数的值)为4

现在已经绘制出了立方体两个相邻的面,但是运行程序发现,因为角度问题只能看到前面的面。

在这里插入图片描述

下面通过使用透视投影矩阵对顶点进行变换来改变显示图形的角度

在调用绘制函数的这两行代码前添加如下代码:

 QMatrix4x4 matrix;
    matrix.perspective(45.0f, (GLfloat)w/(GLfloat)h, 0.1f, 100.0f);
    matrix.translate(0, 0, -3);
    matrix.rotate(-60, 0, 1, 0);  //绕Y轴逆时针旋转
    program->setUniformValue("matrix", matrix);
  • QMatrix4x4类可以表示一个3D空间中的4×4变换矩阵,
  • perspective()函数用来设置透视投影矩阵,这里设置了视角为45°,纵横比为窗口的纵横比,最近的位置为0.1,最远的位置为100。
  • translate()函数平移X、Y和Z轴,这里将Z轴平移-3,即向屏幕里移动
  • rotate()可以设置旋转角度,4个参数分别用来设置角度和X,Y,Z轴,比如这里将Y轴设置为1,就是绕Y轴旋转﹐角度为-60,也就是逆时针旋转60°;
    如果角度为正值,则就是顺时针旋转
  • 最后使用setUniformValue()函数将矩阵关联到顶点着色器的matrix变量

下面将顶点着色器源码更改如下:

    // 创建顶点着色器
    QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    const char *vsrc =
             "#version 130\n"
            "in vec4 vPosition;                        \n"
            "in vec4 vColor;                           \n"
            "out vec4 color;                           \n"
            "uniform mat4 matrix;                      \n"
            "void main() {                             \n"
            "   color = vColor;                        \n"
            "   gl_Position = matrix * vPosition;      \n"
            "}                                         \n";
    vshader->compileSourceCode(vsrc);

这里声明了一个matrix变量,使用了uniform 存储限制符,表明该变量不会在处理过程中发生变化,着色器无法写入到uniform变量,也无法改变它的值。

可以通过setUniformValue()函数来为uniform变量设置值。

gl_Position = matrix * vPosition;

最后在着色器main()函数中进行了矩阵与顶点的乘法运算,注意,矩阵应该在左侧而顶点在右侧。

//表明将输入的顶点位置与顶点的乘法运算到顶点着色器的指定输出位置gl_Position中。
“gl_Position = matrix * vPosition”
//确定位置


现在运行程序就已经可以看到3D立体效果了:

在这里插入图片描述


纹理贴图

前面的程序中生成了正方体的2个面,为了实现更加真实的3D效果,还可以使用图片作为2个面的纹理贴图

Qt的 QOpenGLTexture类封装了一个 OpenGL纹理对象,可以使用该类来设置纹理

该部分内容可以参考Textures Example示例程序。

项目源码:myopengl)在前面的程序中打开myopenglwidget.h文件,添加类前置声明:

#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLBuffer>

class QOpenGLShaderProgram;
class QOpenGLTexture;

这里是使用了QOpenGLTexture类:

然后添加一个私有变量:

private:
    QOpenGLTexture *textures[2];

下面到myopenglwidget.cpp文件中添加头文件:

#include <QOpenGLTexture>

然后在initializeGL()函数的开始部分对变量进行初始化:

void MyOpenGLWidget::initializeGL()
{
    for (int i = 0; i < 2; ++i)
        textures[i] = new QOpenGLTexture(QImage(QString("../myopengl/side%1.png")
                                                .arg(i + 1)).mirrored());

这里需要将两张图片复制到源码目录下。下面更改顶点着色器的源码如下:

const char *vsrc =      
            "in vec4 vPosition;                        \n"
            "in vec4 vTexCoord;                        \n"
            "out vec4 texCoord;                        \n"
            "uniform mat4 matrix;                      \n"
            "void main() {                             \n"
            "   texCoord = vTexCoord;                  \n"
            "   gl_Position = matrix * vPosition;      \n"
            "}                                         \n";

texCoord = vTexCoord;
这里就是将前面的颜色相关变量换成了纹理相关变量,VTexCoord 用来输入纹理坐标

对应的,将片段着色器源码更改如下:

  const char *fsrc =
            "uniform sampler2D tex;                     \n"
            "in vec4 texCoord;                          \n"
            "out vec4 fColor;                           \n"
            "void main() {                              \n"
            "   fColor = texture(tex, texCoord);        \n"
            "}                                          \n";

这里声明了一个sampler2D类型的采样器变量tex,然后在main()函数中使用texture()纹理函数,采样器tex 会以texCoord表示的纹理坐标进行采样,该函数返回包括采样的纹理数据的向量

paintGL()函数将前面设置顶点颜色数组的相关代码更改如下:

   // 纹理坐标
    GLfloat coords[2][4][2] = {
        { {0.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f} },
        { {0.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f} }
    };
    vbo.write(24*sizeof(GLfloat), coords, 16*sizeof(GLfloat));
    GLuint vTexCoord = program->attributeLocation("vTexCoord");
    program->setAttributeBuffer(vTexCoord, GL_FLOAT, 24*sizeof(GLfloat), 2, 0);
    glEnableVertexAttribArray(vTexCoord);
    program->setUniformValue("tex", 0);

这里就是将顶点颜色的相关设置更换为纹理坐标的设置

纹理顶点设置了X和Y坐标,可以简单地这样理解:

  • 对于X坐标,0.0表示纹理的左侧,0.5表示纹理的中点,1.0表示纹理的右侧;
  • 对于Y坐标,0.0表示纹理的底部,0.5表示纹理的中点,1.0表示纹理的顶部。

需要将纹理的4个顶点正确对应到正方形的4个顶点上。

下面更改绘制函数如下:

// 绘制
    for(int i=0; i<2; i++) {
        textures[i]->bind();
        glDrawArrays(GL_TRIANGLE_FAN, i*4, 4);
    }

QOpenGLTexture类的 bind()函数可以将纹理绑定到当前活动纹理单元来准备渲染。

现在运行程序可以看到,已经在两面正方形上使用了指定的图片。

在这里插入图片描述


为了更方便地查看3D效果,下面来实现使用按键控制图形旋转。

(项目源码myopengl)首先在 myopenglwidget.h文件中添加键盘按下事件处理函数的声明:

  void keyPressEvent(QKeyEvent *event);

添加private变量:

private:
    QOpenGLShaderProgram *program;
    QOpenGLBuffer vbo;
    QOpenGLTexture *textures[2];
    GLfloat translate, xRot, yRot, zRot;

下面到myopenglwidget.cpp文件中先添加头文件#include <QKeyEvent>,然后在构造函数中初始化变量:

MyOpenGLWidget::MyOpenGLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{
    translate = -6.0;
    xRot = zRot = 0.0;
    yRot = -30.0;
}

到paintGL)函数中更改设置矩阵的相关代码如下:

 QMatrix4x4 matrix;
    matrix.perspective(45.0f, (GLfloat)w/(GLfloat)h, 0.1f, 100.0f);
    matrix.translate(0, 0, translate);
    matrix.rotate(xRot, 1.0, 0.0, 0.0);
    matrix.rotate(yRot, 0.0, 1.0, 0.0);
    matrix.rotate(zRot, 0.0, 0.0, 1.0);
    program->setUniformValue("matrix", matrix);

然后添加键盘按下事件处理函数的定义:

void MyOpenGLWidget::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_Up:
        xRot += 10;
        break;
    case Qt::Key_Left:
        yRot += 10;
        break;
    case Qt::Key_Right:
        zRot += 10;
        break;
    case Qt::Key_Down:
        translate -= 1;
        break;
    case Qt::Key_Space:
        translate += 1;
        break;
    default:
        break;
    }
    update();
    QOpenGLWidget::keyPressEvent(event);
}

现在运行程序,则可以通过键盘方向键和空格键来控制图形的显示。

为了拥有更好的显示效果,可以开启深度测试,下面在initializeGL()函数中initializeOpenGL-Functions()函数调用之后添加如下代码:

   // 启用深度测试
    glEnable(GL_DEPTH_TEST);

全部代码如下:

void MyOpenGLWidget::initializeGL()
{
    for (int i = 0; i < 2; ++i)
        textures[i] = new QOpenGLTexture(QImage(QString("../myopengl/side%1.png")
                                                .arg(i + 1)).mirrored());
    // 为当前环境初始化OpenGL函数
    initializeOpenGLFunctions();

    // 启用深度测试
    glEnable(GL_DEPTH_TEST);

    // 创建顶点着色器
    QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    const char *vsrc =
            "in vec4 vPosition;                        \n"
            "in vec4 vTexCoord;                        \n"
            "out vec4 texCoord;                        \n"
            "uniform mat4 matrix;                      \n"
            "void main() {                             \n"
            "   texCoord = vTexCoord;                  \n"
            "   gl_Position = matrix * vPosition;      \n"
            "}                                         \n";
    vshader->compileSourceCode(vsrc);
    // 创建片段着色器
    QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
    const char *fsrc =
            "uniform sampler2D tex;                     \n"
            "in vec4 texCoord;                          \n"
            "out vec4 fColor;                           \n"
            "void main() {                              \n"
            "   fColor = texture(tex, texCoord);        \n"
            "}                                          \n";
    fshader->compileSourceCode(fsrc);

    // 创建着色器程序
    program = new QOpenGLShaderProgram;
    program->addShader(vshader);
    program->addShader(fshader);

    program->link();
    program->bind();

}

再次运行可以看到3D图形效果:
在这里插入图片描述
可以通过键盘方向键和空格键来控制图形的显示。就是选择方向。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐