您当前的位置: 首页 >  qt

龚建波

暂无认证

  • 4浏览

    0关注

    313博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Qt示例学习:OpenGL Under QML

龚建波 发布时间:2022-05-14 16:45:19 ,浏览量:4

0.前言

Qt Creator 中提供了两个 QML 和 OpenGL 混合使用的示例。示例 "fbo" 是在帧缓冲对象(FBO)上渲染小部件,可以和已有的 QML Item 混合使用;示例 "opengl under qml" 是在 QML Item 渲染之前或之后绘制自定义的 OpenGL 内容,是对整个 Window 操作,和 QML Item 一起用比较麻烦,不是处于最底层就是浮在表面。

在 Qt Creator 搜索 opengl under qml,或者源码示例中查找

Qt 安装后示例路径:E:\QtOnline\Examples\Qt-5.15.2\quick\scenegraph\openglunderqml

Qt 源码路径:E:\qt-everywhere-src-5.15.2\qtdeclarative\examples\quick\scenegraph\openglunderqml\openglunderqml.pro

示例文档:https://doc.qt.io/qt-5/qtquick-scenegraph-openglunderqml-example.html 

注释连接:https://github.com/gongjianbo/MyTestCode/tree/master/Qml/QtExampleOpenGLUnderQML

1.示例学习 

该示例比较简单,就是关联 Window 的渲染信号,然后在渲染线程调用 OpenGL 相关接口进行绘制。Qt5.14/Qt5.15 的时候因为接口变动(引入了 Qt RHI,Qt 的图形抽象层),示例有一些修改,主要是资源释放和状态保存。

示例由两个类组成:继承自 QQuickItem 的 Squircle 类,和简易封装 OpenGL 绘制的 Render 类。Render  的相关接口都是 Item 关联 Window 的相关信号来触发调用的。

首先触发的是 QQuickItem::windowChanged 信号,我们将 Item 添加到 Window 或者移除,都会触发该信号。拿到 Window 指针后,就可以关联 QuickWindow  的一些信号,用于状态同步、绘制、资源释放等的调用(渲染相关信号基本都是渲染线程发出的,所以信号槽连接方式使用 Qt::DirectConnection)。

void Squircle::handleWindowChanged(QQuickWindow *win)
{
    if (win) {
        //因为UI线程和渲染线程可能不是同一个,所以Qt::DirectConnection连接,使在渲染线程绘制
        //render之前同步状态,在渲染线程
        connect(win, &QQuickWindow::beforeSynchronizing, this, &Squircle::sync, Qt::DirectConnection);
        //render之后同步状态,在渲染线程
        //connect(win, &QQuickWindow::afterSynchronizing, this, &Squircle::sync, Qt::DirectConnection);
        //释放,在渲染线程
        connect(win, &QQuickWindow::sceneGraphInvalidated, this, &Squircle::cleanup, Qt::DirectConnection);
    }
}

void Squircle::sync()
{
    //第一次调用时进行初始化
    if (!m_renderer) {
        m_renderer = new SquircleRenderer();
        //设置了win->setClearBeforeRendering(false)的话beforeRendering时绘制的就被清掉了
        //connect(window(), &QQuickWindow::beforeRendering, m_renderer, &SquircleRenderer::paint, Qt::DirectConnection);
        //scene graph(QML场景图) render之后,swapbuffers前触发,在渲染线程
        //浮在QML已绘制的最上层
        connect(window(), &QQuickWindow::afterRendering, m_renderer, &SquircleRenderer::paint, Qt::DirectConnection);
    }
    //同步 UI 状态给 render ... ...
}

对于这些信号触发的顺序,可以参考文档:scene-graph-and-rendering 

其中 Scene Graph is rendered 这一步就是绘制 QML Window 中 Item 节点树。在此之前的 beforeSynchronizing 信号发出时,我们可以将 UI 中的状态值同步给渲染线程中使用到的变量。在 Scene Graph 渲染前触发 beforeRendering 信号,在此时绘制会处于 QML Item 的底层(需要设置 window->setClearBeforeRendering(false),不然此时绘制的会被接下来的 glClear 给清除);在 Scene Graph 渲染后触发 afterRendering 信号,在此时绘制会浮在 QML Item 的上层。

在关联 rendering 信号的槽函数中,我们进行 OpenGL 的绘制操作,默认这时是对整个 Window 操作的。绘制完后,调用 window->resetOpenGLState() 重置上下文状态。Qt5.14/Qt5.15 版的示例此处有变动,信号新增了 beforeRenderPassRecording,还要求在绘制前后调用 window->beginExternalCommands() 和 window->endExternalCommands(),文档中的说明是避免破坏状态。

void SquircleRenderer::paint()
{
    // Play nice with the RHI. Not strictly needed when the scenegraph uses
    // OpenGL directly.
    m_window->beginExternalCommands();
    //绘制操作 ... ...
    // Not strictly needed for this example, but generally useful for when
    // mixing with raw OpenGL.
    m_window->resetOpenGLState();
    m_window->endExternalCommands();
}

最后是释放操作, 关联 sceneGraphInvalidated 信号进行资源的释放,示例在此 delete Render。Qt5.14/Qt5.15 版的示例,QQuickItem 增加了 releaseResources 虚函数接口做释放。

class CleanupJob : public QRunnable
{
public:
    CleanupJob(SquircleRenderer *renderer) : m_renderer(renderer) { }
    void run() override { delete m_renderer; }
private:
    SquircleRenderer *m_renderer;
};

void Squircle::releaseResources()
{
    window()->scheduleRenderJob(new CleanupJob(m_renderer), QQuickWindow::BeforeSynchronizingStage);
    m_renderer = nullptr;
}

这个示例主要是学习 Qt Quick 的渲染流程,实用性对我个人而言不是很大。 

2.主要代码注释

(贴在这里便于以后我自己查看)

#pragma once
//此工程为Qt5.12的Demo修改,Qt5.15示例有一些变更
//变动1:
//在QQuickItem::releaseResources中
//调用window()->scheduleRenderJob()释放render
//之前的QQuickWindow::sceneGraphInvalidated逻辑还保留在,但是demo没触发信号
//变动2:
//绘制前调用window->beginExternalCommands()
//绘制后调用window->endExternalCommands()
//文档说是避免破坏状态
//
//示例安装路径:E:\Qt\QtOnline\Examples\Qt-5.15.2\quick\scenegraph\openglunderqml
//示例源码路径:E:\Qt\qt-everywhere-src-5.15.2\qtdeclarative\examples\quick\scenegraph\openglunderqml\openglunderqml.pro
//参考文档:https://doc.qt.io/qt-5/qquickitem.html
//参考文档:https://doc.qt.io/qt-5/qquickwindow.html
//参考文档:https://doc.qt.io/qt-5/qtquick-visualcanvas-scenegraph.html#scene-graph-and-rendering
//参考文档:https://doc.qt.io/qt-5/qtquick-scenegraph-openglunderqml-example.html
#include 
#include 
#include 

//示例做了个简单封装,将渲染部分单独提出来
class SquircleRenderer : public QObject, protected QOpenGLFunctions
{
    Q_OBJECT
public:
    SquircleRenderer() {}
    ~SquircleRenderer();

    void setT(qreal t) { m_t = t; }
    void setViewport(const QRect &size) { m_viewport = size; }
    void setWindow(QQuickWindow *window) { m_window = window; }

public slots:
    void paint();

private:
    QRect m_viewport;
    qreal m_t = 0;
    QOpenGLShaderProgram *m_program = nullptr;
    QQuickWindow *m_window = nullptr;
};

//方圆形(squircle)一词是方形(square)与圆形(circle)的融合
//比圆角矩形(roundrect)看起来更圆一点
class Squircle : public QQuickItem
{
    Q_OBJECT
    //自定义属性t,渲染时用于计算渐变色,UI中动画循环更新其值
    Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged)
public:
    Squircle();

    qreal t() const { return m_t; }
    void setT(qreal t);

signals:
    void tChanged();

public slots:
    void sync();
    void cleanup();

private slots:
    void handleWindowChanged(QQuickWindow *win);

private:
    qreal m_t = 0;
    //绘制简单的封装到一个类中
    SquircleRenderer *m_renderer = nullptr;
};
#include "squircle.h"

#include 
#include 
#include 
#include 
#include 

SquircleRenderer::~SquircleRenderer()
{
    if (m_program) {
        delete m_program;
        m_program = nullptr;
    }
}

//关联信号QQuickWindow::before/afterRendering绘制
void SquircleRenderer::paint()
{
    if (!m_window) {
        return;
    }
    //示例绘制了一个简单的squircle渐变图案
    //在此基础上我加了一个glScissor只绘制Item所在区域,避免绘到整个Window
    if (!m_program) {
        //初始化OpenGL上下文
        initializeOpenGLFunctions();
        //初始化着色器程序
        m_program = new QOpenGLShaderProgram();
        m_program->addCacheableShaderFromSourceCode(
                    QOpenGLShader::Vertex,
                    "attribute highp vec4 vertices;"
                    "varying highp vec2 coords;"
                    "void main() {"
                    "    gl_Position = vertices;"
                    "    coords = vertices.xy;"
                    "}");
        m_program->addCacheableShaderFromSourceCode(
                    QOpenGLShader::Fragment,
                    "uniform lowp float t;"
                    "varying highp vec2 coords;"
                    "void main() {"
                    "    lowp float i = 1. - (pow(abs(coords.x), 4.) + pow(abs(coords.y), 4.));"
                    "    i = smoothstep(t - 0.8, t + 0.8, i);"
                    "    i = floor(i * 20.) / 20.;"
                    "    gl_FragColor = vec4(coords * .5 + .5, i, i);"
                    "}");

        m_program->bindAttributeLocation("vertices", 0);
        m_program->link();
    }

    m_program->bind();
    m_program->enableAttributeArray(0);

    float values[] = {
        -1, -1,
        1, -1,
        -1, 1,
        1, 1
    };
    m_program->setAttributeArray(0, GL_FLOAT, values, 2);
    m_program->setUniformValue("t", (float) m_t);

    int w = m_viewport.width();
    int h = m_viewport.height();
    int x = m_viewport.x();
    int y = m_viewport.y();
    glViewport(x, y , w, h);
    //只绘制Item所在区域,避免绘到整个Window
    glEnable(GL_SCISSOR_TEST);
    glScissor(x, y , w, h);

    glDisable(GL_DEPTH_TEST);

    glClearColor(0, 1, 0, 1);
    glClear(GL_COLOR_BUFFER_BIT);

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);

    //两个三角的三角带,即一个矩形
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    glDisable(GL_SCISSOR_TEST);

    m_program->disableAttributeArray(0);
    m_program->release();

    //调用此函数将 OpenGL 上下文重置为默认值/状态
    //此函数不会触及固定函数管道中的状态
    //此函数不会清除颜色、深度和模板缓冲区
    //使用QQuickWindow::setClearBeforeRendering来控制颜色缓冲区的清除
    //场景图渲染器可能会破坏深度和模板缓冲区,按需手动清除这些
    m_window->resetOpenGLState();
}

Squircle::Squircle()
{
    //itemChange函数中触发windowChanged
    //此示例仅在item添加到Window和移除时触发
    connect(this, &QQuickItem::windowChanged, this, &Squircle::handleWindowChanged);
}

void Squircle::setT(qreal t)
{
    if (t == m_t) {
        return;
    }
    m_t = t;
    emit tChanged();
    //UI动画更新属性值后,调用update刷新
    if (window()) {
        window()->update();
    }
}

//关联信号QQuickWindow::before/afterSynchronizing同步状态
void Squircle::sync()
{
    //第一次调用时进行初始化
    if (!m_renderer) {
        m_renderer = new SquircleRenderer();
        //设置了win->setClearBeforeRendering(false)的话beforeRendering时绘制的就被清掉了
        //connect(window(), &QQuickWindow::beforeRendering, m_renderer, &SquircleRenderer::paint, Qt::DirectConnection);
        //scene graph(QML场景图) render之后,swapbuffers前触发,在渲染线程
        //浮在QML已绘制的最上层
        connect(window(), &QQuickWindow::afterRendering, m_renderer, &SquircleRenderer::paint, Qt::DirectConnection);
    }
    //同步 UI 状态给 render ... ...
    //设置Item在Window中的位置,对应OpenGL的viewport以左下角为起点
    //示例是全屏,我这里设置为一个小方块
    m_renderer->setViewport(QRect(x(), window()->height() - y() - height(), width(), height()));
    m_renderer->setT(m_t);
    //render类中调用了window的接口
    m_renderer->setWindow(window());
}

//关联信号QQuickWindow::sceneGraphInvalidated释放
void Squircle::cleanup()
{
    if (m_renderer) {
        delete m_renderer;
        m_renderer = nullptr;
    }
}

//关联信号QQuickItem::windowChanged
void Squircle::handleWindowChanged(QQuickWindow *win)
{
    if (win) {
        //因为UI线程和渲染线程可能不是同一个,所以Qt::DirectConnection连接,使在渲染线程绘制
        //render之前同步状态,在渲染线程
        connect(win, &QQuickWindow::beforeSynchronizing, this, &Squircle::sync, Qt::DirectConnection);
        //render之后同步状态,在渲染线程
        //connect(win, &QQuickWindow::afterSynchronizing, this, &Squircle::sync, Qt::DirectConnection);
        //释放,在渲染线程
        connect(win, &QQuickWindow::sceneGraphInvalidated, this, &Squircle::cleanup, Qt::DirectConnection);
        //render前clear
        //如果设置为false拖动窗口大小会有残影
        //而且QML Window的color就是clear填充的
        //如果设置为true则QQuickWindow::beforeRendering时绘制会被clear清掉
        //win->setClearBeforeRendering(false);
    }
}

关注
打赏
1655829268
查看更多评论
立即登录/注册

微信扫码登录

0.0371s