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);
}
}