シーングラフ - QML下のOpenGL

Qt Quick シーン下で OpenGL をレンダリングする方法を示します。

OpenGL under QML の例では、アプリケーションがQQuickWindow::beforeRendering() シグナルを利用して、Qt Quick シーンの下にカスタム OpenGL コンテンツを描画する方法を示します。このシグナルは、シーングラフがレンダリングを開始する前に、各フレームの開始時に発せられます。したがって、このシグナルへの応答として行われるOpenGLの描画コールは、Qt Quickアイテムの下でスタックされます。

別の方法として、Qt Quickシーンの上にOpenGLコンテンツをレンダリングしたいアプリケーションは、QQuickWindow::afterRendering ()シグナルに接続することでレンダリングできます。

この例では、OpenGLレンダリングに影響を与えるような値をQMLに公開することも可能であることを確認します。QMLファイル内のNumberAnimation を使ってしきい値のアニメーションを行い、この値をOpenGLシェーダプログラムが使ってスクエアを描画します。

この例は、Direct3D 11 Under QMLMetal Under QMLVulkan Under QMLの例とほとんど同じで、ネイティブAPIが異なるだけで、同じカスタムコンテンツをレンダリングします。

class Squircle : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged)
    QML_ELEMENT

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:
    void releaseResources() override;

    qreal m_t;
    SquircleRenderer *m_renderer;
};

まず、QMLに公開するオブジェクトが必要です。これはQQuickItem のサブクラスで、QQuickItem::window() に簡単にアクセスできます。QML_ELEMENT マクロを使ってQMLに公開します。

class SquircleRenderer : public QObject, protected QOpenGLFunctions
{
    Q_OBJECT
public:
    ~SquircleRenderer();

    void setT(qreal t) { m_t = t; }
    void setViewportSize(const QSize &size) { m_viewportSize = size; }
    void setWindow(QQuickWindow *window) { m_window = window; }

public slots:
    void init();
    void paint();

private:
    QSize m_viewportSize;
    qreal m_t = 0.0;
    QOpenGLShaderProgram *m_program = nullptr;
    QQuickWindow *m_window = nullptr;
};

次に、レンダリングを行うオブジェクトが必要です。アイテムはGUIスレッドにあり、レンダリングはレンダースレッドで行われる可能性があるため、このインスタンスはQQuickItem から分離する必要があります。QQuickWindow::beforeRendering ()に接続したいので、レンダラーをQObject 。レンダラーには、GUIスレッドから独立して、必要なすべての状態のコピーが含まれている。

注意: 2つのオブジェクトを1つに統合したくならないようにしてください。QQuickItemsは、レンダリングスレッドがレンダリング中にGUIスレッド上で削除される可能性があります。

実装に移りましょう。

Squircle::Squircle()
    : m_t(0)
    , m_renderer(nullptr)
{
    connect(this, &QQuickItem::windowChanged, this, &Squircle::handleWindowChanged);
}

Squircle クラスのコンストラクタは単に値を初期化し、レンダラの準備に使用するウィンドウ変更シグナルに接続します。

void Squircle::handleWindowChanged(QQuickWindow *win)
{
    if (win) {
        connect(win, &QQuickWindow::beforeSynchronizing, this, &Squircle::sync, Qt::DirectConnection);
        connect(win, &QQuickWindow::sceneGraphInvalidated, this, &Squircle::cleanup, Qt::DirectConnection);

ウィンドウができたら、QQuickWindow::beforeSynchronizing ()シグナルに接続します。このシグナルは、レンダラーを作成し、ステートを安全にコピーするために使用します。また、QQuickWindow::sceneGraphInvalidated ()シグナルに接続して、レンダラーのクリーンアップを処理します。

注意: SquircleオブジェクトはGUIスレッドと親和性があり、シグナルはレンダリングスレッドから発信されるため、Qt::DirectConnection で接続することが非常に重要です。これを怠ると、OpenGLコンテキストが存在しない間違ったスレッドでスロットが呼び出されることになります。

        // Ensure we start with cleared to black. The squircle's blend mode relies on this.
        win->setColor(Qt::black);
    }
}

シーングラフのデフォルトの動作は、レンダリングの前にフレームバッファをクリアすることです。このクリアがエンキューされた後に、独自のレンダリングコードを挿入するので、これは問題ありません。ただし、希望の色(黒)にクリアすることを確認してください。

void Squircle::sync()
{
    if (!m_renderer) {
        m_renderer = new SquircleRenderer();
        connect(window(), &QQuickWindow::beforeRendering, m_renderer, &SquircleRenderer::init, Qt::DirectConnection);
        connect(window(), &QQuickWindow::beforeRenderPassRecording, m_renderer, &SquircleRenderer::paint, Qt::DirectConnection);
    }
    m_renderer->setViewportSize(window()->size() * window()->devicePixelRatio());
    m_renderer->setT(m_t);
    m_renderer->setWindow(window());
}

sync() 関数を使用してレンダラーを初期化し、アイテムの状態をレンダラーにコピーします。レンダラーが作成されたら、QQuickWindow::beforeRendering ()とQQuickWindow::beforeRenderPassRecording ()をレンダラーのinit() ()とpaint() ()スロットに接続します。

注: QQuickWindow::beforeSynchronizing() シグナルは、GUI スレッドがブロックされている間にレンダリング スレッドで発行されるため、追加保護なしで値をコピーするだけでも安全です。

void Squircle::cleanup()
{
    delete m_renderer;
    m_renderer = nullptr;
}

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

SquircleRenderer::~SquircleRenderer()
{
    delete m_program;
}

cleanup() 関数では、レンダラーを削除し、レンダラー自身のリソースをクリーンアップします。sceneGraphInvalidated()シグナルに接続するだけではすべてのケースに対応できないため、QQuickWindow::releaseResources ()を再実装することで補完している。

void Squircle::setT(qreal t)
{
    if (t == m_t)
        return;
    m_t = t;
    emit tChanged();
    if (window())
        window()->update();
}

t の値が変更された場合、QQuickItem::update() ではなく、QQuickWindow::update() を呼び出します。前者は、シーングラフが最後のフレームから変更されていない場合でも、ウィンドウ全体を強制的に再描画するためです。

void SquircleRenderer::init()
{
    if (!m_program) {
        QSGRendererInterface *rif = m_window->rendererInterface();
        Q_ASSERT(rif->graphicsApi() == QSGRendererInterface::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();

    }
}

SquircleRendererのinit() 関数では、シェーダープログラムを初期化することから始めます。OpenGLコンテキストは、スロットが呼び出されたときのスレッドで現在のものです。

void SquircleRenderer::paint()
{
    // Play nice with the RHI. Not strictly needed when the scenegraph uses
    // OpenGL directly.
    m_window->beginExternalCommands();

    m_program->bind();

    m_program->enableAttributeArray(0);

    float values[] = {
        -1, -1,
        1, -1,
        -1, 1,
        1, 1
    };

    // This example relies on (deprecated) client-side pointers for the vertex
    // input. Therefore, we have to make sure no vertex buffer is bound.
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    m_program->setAttributeArray(0, GL_FLOAT, values, 2);
    m_program->setUniformValue("t", (float) m_t);

    glViewport(0, 0, m_viewportSize.width(), m_viewportSize.height());

    glDisable(GL_DEPTH_TEST);

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

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

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

    m_window->endExternalCommands();
}

シェーダープログラムを使用して、paint() でスクサークルを描画します。

int main(int argc, char **argv)
{
    QGuiApplication app(argc, argv);

    QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);

    QQuickView view;
    view.setResizeMode(QQuickView::SizeRootObjectToView);
    view.setSource(QUrl("qrc:///scenegraph/openglunderqml/main.qml"));
    view.show();

    return QGuiApplication::exec();
}

アプリケーションのmain() 関数はQQuickView をインスタンス化し、main.qml ファイルを起動します。

import QtQuick
import OpenGLUnderQML

Item {

    width: 320
    height: 480

    Squircle {
        SequentialAnimation on t {
            NumberAnimation { to: 1; duration: 2500; easing.type: Easing.InQuad }
            NumberAnimation { to: 0; duration: 2500; easing.type: Easing.OutQuad }
            loops: Animation.Infinite
            running: true
        }
    }

main() 関数で登録した名前で Squircle QML タイプをインポートします。そしてインスタンス化し、t プロパティに実行中のNumberAnimation を作成します。

    Rectangle {
        color: Qt.rgba(1, 1, 1, 0.7)
        radius: 10
        border.width: 1
        border.color: "white"
        anchors.fill: label
        anchors.margins: -10
    }

    Text {
        id: label
        color: "black"
        wrapMode: Text.WordWrap
        text: qsTr("The background here is a squircle rendered with raw OpenGL using the 'beforeRender()' signal in QQuickWindow. This text label and its border is rendered using QML")
        anchors.right: parent.right
        anchors.left: parent.left
        anchors.bottom: parent.bottom
        anchors.margins: 20
    }
}

それから、短い説明テキストをオーバーレイして、Qt Quickシーンの下でOpenGLをレンダリングしていることがはっきりわかるようにします。

プロジェクト例 @ code.qt.io

©2024 The Qt Company Ltd. ここに含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。