Scene Graph - カスタムジオメトリ

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 でよく使われるグラフィックスは、いくつかの標準的な属性セットが中心となっているため、デフォルトでこれらの属性セットが提供されています。ここでは、2つの浮動小数点数(1つはx座標、もう1つはy座標)を持つ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

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.