シーングラフ - カスタムジオメトリ

Qt Quick Scene Graph でカスタムジオメトリを実装する方法を紹介します。

カスタムジオメトリの例では、シーングラフ API を使用してシーングラフ用のカスタムジオメトリを構築するQQuickItem の作成方法を示します。これは、CustomGeometry モジュールの一部となるBezierCurve アイテムを作成し、QML ファイルで利用します。

BezierCurve 宣言

#include <QtQuick/QQuickItem>

class BezierCurve : public QQuickItem
{
    Q_OBJECT

    Q_PROPERTY(QPointF p1 READ p1 WRITE setP1 NOTIFY p1Changed)
    Q_PROPERTY(QPointF p2 READ p2 WRITE setP2 NOTIFY p2Changed)
    Q_PROPERTY(QPointF p3 READ p3 WRITE setP3 NOTIFY p3Changed)
    Q_PROPERTY(QPointF p4 READ p4 WRITE setP4 NOTIFY p4Changed)

    Q_PROPERTY(int segmentCount READ segmentCount WRITE setSegmentCount NOTIFY segmentCountChanged)
    QML_ELEMENT

public:
    BezierCurve(QQuickItem *parent = nullptr);
    ~BezierCurve();

    QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;

    QPointF p1() const { return m_p1; }
    QPointF p2() const { return m_p2; }
    QPointF p3() const { return m_p3; }
    QPointF p4() const { return m_p4; }

    int segmentCount() const { return m_segmentCount; }

    void setP1(const QPointF &p);
    void setP2(const QPointF &p);
    void setP3(const QPointF &p);
    void setP4(const QPointF &p);

    void setSegmentCount(int count);

signals:
    void p1Changed(const QPointF &p);
    void p2Changed(const QPointF &p);
    void p3Changed(const QPointF &p);
    void p4Changed(const QPointF &p);

    void segmentCountChanged(int count);

private:
    QPointF m_p1;
    QPointF m_p2;
    QPointF m_p3;
    QPointF m_p4;

    int m_segmentCount;
};

アイテム宣言はQQuickItem クラスをサブクラス化し、5つのプロパティを追加します。ひとつはベジェ曲線の4つの制御点、もうひとつは曲線を細分化するセグメントの数を制御するパラメータである。それぞれのプロパティに対応するゲッター関数とセッター関数があります。これらのプロパティはQMLでバインドすることができるため、それぞれのプロパティに対してノーティファイアシグナルを用意し、変更がQMLエンジンに拾われ、それに応じて使用されるようにすることも望ましい。

    QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;

QMLシーンとレンダリングシーングラフの同期ポイントは仮想関数QQuickItem::updatePaintNode()です。

注意: 多くのハードウェア構成では、シーングラフは別スレッドでレンダリングされます。そのため、シーングラフとのやりとりは、何よりもまずQQuickItem::updatePaintNode() 関数を通して、制御された方法で行われることが重要です。

ベジェ曲線の実装

BezierCurve::BezierCurve(QQuickItem *parent)
    : QQuickItem(parent)
    , m_p1(0, 0)
    , m_p2(1, 0)
    , m_p3(0, 1)
    , m_p4(1, 1)
    , m_segmentCount(32)
{
    setFlag(ItemHasContents, true);
}

BezierCurve コンストラクタは、制御点とセグメント数のデフォルト値を設定します。ベジェ曲線は、アイテムの外接矩形に対する正規化座標で指定されます。

コンストラクタはまた、フラグQQuickItem::ItemHasContents を設定します。このフラグは、このアイテムがビジュアルコンテンツを提供し、QMLシーンがレンダリングシーングラフと同期する時にQQuickItem::updatePaintNode() を呼び出すことを canvas に伝えます。

BezierCurve::~BezierCurve() = default;

BezierCurveクラスにはクリーンアップが必要なデータメンバーはないので、デストラクタは何もしません。レンダリングシーングラフはシーングラフ自身によって管理され、異なるスレッドで管理される可能性があるため、QQuickItem クラスのQSGNode 参照を保持したり、明示的にクリーンアップしようとしてはいけません。

void BezierCurve::setP1(const QPointF &p)
{
    if (p == m_p1)
        return;

    m_p1 = p;
    emit p1Changed(p);
    update();
}

p1プロパティのセッター関数は、値が変更されていないかどうかをチェックし、変更されていない場合は早めに終了する。その後、内部値を更新し、変更されたシグナルを出力します。その後、QQuickItem::update ()関数の呼び出しに進み、このオブジェクトの状態が変更され、レンダリングシーングラフと同期する必要があることをレンダリングシーングラフに通知する。update()を呼び出すと、後でQQuickItem::updatePaintNode()を呼び出すことになる。

他のプロパティ・セッタは同等であり、この例では省略されている。

QSGNode *BezierCurve::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
    QSGGeometryNode *node = nullptr;
    QSGGeometry *geometry = nullptr;

    if (!oldNode) {
        node = new QSGGeometryNode;

updatePaintNode()関数は、QMLシーンの状態をレンダリングシーングラフと同期させるための主要な統合ポイントです。この関数にはQSGNode が渡されます。 は、最後にこの関数を呼び出したときに返されたインスタンスです。この関数が最初に呼び出されたときはNULLで、QSGGeometryNode を作成し、ジオメトリとマテリアルで塗りつぶします。

        geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), m_segmentCount);
        geometry->setLineWidth(2);
        geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
        node->setGeometry(geometry);
        node->setFlag(QSGNode::OwnsGeometry);

次にジオメトリを作成し、ノードに追加します。QSGGeometry コンストラクタの最初の引数は頂点タイプの定義で、"アトリビュートセット "と呼ばれます。QML でよく使われるグラフィックスは、いくつかの標準的な属性セットが中心となっているため、デフォルトでこれらの属性セットが提供されています。ここでは、x座標とy座標の2つのfloatを持つPoint2D属性セットを使用します。番目の引数は頂点数です。

カスタム属性セットも作成できますが、この例では扱いません。

ジオメトリのメモリ管理については特に必要ないので、QSGGeometryNode がジオメトリを所有するように指定します。

割り当てを最小限に抑え、メモリの断片化を減らし、パフォーマンスを向上させるために、ジオメトリをQSGGeometryNode サブクラスのメンバにすることも可能です。この場合、QSGGeometryNode::OwnsGeometry フラグは設定しません。

        auto *material = new QSGFlatColorMaterial;
        material->setColor(QColor(255, 0, 0));
        node->setMaterial(material);
        node->setFlag(QSGNode::OwnsMaterial);

シーングラフ API には、よく使用されるマテリアルの実装がいくつか用意されています。この例では、QSGFlatColorMaterial を使用します。これは、ジオメトリによって定義された形状をソリッドカラーで塗りつぶします。ここでもマテリアルのオーナーシップをノードに渡すことで、シーングラフがマテリアルをクリーンアップできるようにしています。

    } else {
        node = static_cast<QSGGeometryNode *>(oldNode);
        geometry = node->geometry();
        geometry->allocate(m_segmentCount);
    }

QML アイテムが変更され、既存のノードのジオメトリのみを変更したい場合は、oldNodeQSGGeometryNode インスタンスにキャストし、そのジオメトリを抽出します。セグメント数が変更された場合は、QSGGeometry::allocate() を呼び出し、頂点数が正しいことを確認します。

    QSizeF itemSize = size();
    QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
    for (int i = 0; i < m_segmentCount; ++i) {
        qreal t = i / qreal(m_segmentCount - 1);
        qreal invt = 1 - t;

        QPointF pos = invt * invt * invt * m_p1
                    + 3 * invt * invt * t * m_p2
                    + 3 * invt * t * t * m_p3
                    + t * t * t * m_p4;

        float x = pos.x() * itemSize.width();
        float y = pos.y() * itemSize.height();

        vertices[i].set(x, y);
    }
    node->markDirty(QSGNode::DirtyGeometry);

ジオメトリを埋めるために、まず頂点配列を取り出します。デフォルトの属性セットを使用しているので、便利な関数QSGGeometry::vertexDataAsPoint2D() を使用できます。次に、各セグメントを調べて位置を計算し、その値を頂点に書き込みます。

    return node;
}

関数の最後に、シーングラフがレンダリングできるようにノードを返します。

アプリケーションのエントリーポイント

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

    QQuickView view;
    QSurfaceFormat format = view.format();
    format.setSamples(16);
    view.setFormat(format);
    view.setSource(QUrl("qrc:///scenegraph/customgeometry/main.qml"));
    view.show();

    return app.exec();
}

このアプリケーションは、QGuiApplication と、.qmlファイルを渡すQQuickView を持つ、単純なQMLアプリケーションです。

    QML_ELEMENT

BezierCurveを使うには、QML_ELEMENT マクロを使ってQMLエンジンに登録する必要があります。これにより、BezierCurveという名前が与えられ、プロジェクトのビルドファイルで定義されているCustomGeometry 1.0 モジュールの一部になります:

qmake#tab-qmakeベジェ曲線がされます
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

cmake_minimum_required(VERSION 3.16)
project(customgeometry_declarative LANGUAGES CXX)

find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick)

qt_standard_project_setup()

qt_add_executable(customgeometry_declarative WIN32 MACOSX_BUNDLE
    beziercurve.cpp beziercurve.h
    main.cpp
)

target_link_libraries(customgeometry_declarative PRIVATE
    Qt6::Core
    Qt6::Gui
    Qt6::Quick
)

qt_add_qml_module(customgeometry_declarative
    URI CustomGeometry
    QML_FILES main.qml
    RESOURCE_PREFIX /scenegraph/customgeometry
    NO_RESOURCE_TARGET_PATH
)

install(TARGETS customgeometry_declarative
    BUNDLE  DESTINATION .
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

qt_generate_deploy_qml_app_script(
    TARGET customgeometry_declarative
    OUTPUT_SCRIPT deploy_script
    MACOS_BUNDLE_POST_BUILD
    NO_UNSUPPORTED_PLATFORM_ERROR
    DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM
)
install(SCRIPT ${deploy_script})
TARGET = customgeometry
QT += quick

CONFIG += qmltypes
QML_IMPORT_NAME = CustomGeometry
QML_IMPORT_MAJOR_VERSION = 1

SOURCES += \
    main.cpp \
    beziercurve.cpp

HEADERS += \
    beziercurve.h

RESOURCES += customgeometry.qrc

target.path = $$[QT_INSTALL_EXAMPLES]/quick/scenegraph/customgeometry
INSTALLS += target

ベジェ曲線は線分として描画されるため、アンチエイリアスを得るためにビューをマルチサンプリングするように指定します。これは必須ではありませんが、マルチサンプリングをサポートしているハードウェアでは、アイテムの見栄えが少し良くなります。マルチサンプリングはしばしばメモリ使用量が多くなるため、デフォルトでは有効になっていません。

アイテムの使用

import QtQuick
import CustomGeometry

この.qmlファイルは、標準的な型を得るためにQtQuick 2.0 モジュールをインポートし、また新しく作成したBezierCurveオブジェクトを含む独自のCustomGeometry 1.0 モジュールをインポートしている。

Item {
    width: 300
    height: 200

    BezierCurve {
        id: line
        anchors.fill: parent
        anchors.margins: 20

そして、ルート・アイテムと、ルートを埋めるためにアンカーするBezierCurveのインスタンスを作成する。

        property real t
        SequentialAnimation on t {
            NumberAnimation { to: 1; duration: 2000; easing.type: Easing.InOutQuad }
            NumberAnimation { to: 0; duration: 2000; easing.type: Easing.InOutQuad }
            loops: Animation.Infinite
        }

        p2: Qt.point(t, 1 - t)
        p3: Qt.point(1 - t, t)
    }

この例をもう少し面白くするために、曲線の2つの制御点を変更するアニメーションを追加します。端点は変更しません。

    Text {
        anchors.bottom: line.bottom

        x: 20
        width: parent.width - 40
        wrapMode: Text.WordWrap

        text: qsTr("This curve is a custom scene graph item, implemented using line strips")
    }
}

最後に、この例が示していることを説明する短いテキストを重ねます。

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

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