シーングラフ - Metalテクスチャのインポート

Metalで直接作成されたテクスチャの使用方法を示します。

SquircleをMetalテクスチャとしてレンダリングし、Qt Quickアイテムに表示する

「Metal テクスチャのインポート」サンプルでは、アプリケーションがQt Quick シーン内でMTLTextureをインポートして使用する方法を示しています。これは、ネイティブのMetalレンダリングを統合する際、アンダーレイやオーバーレイの手法に代わる選択肢となります。 多くの場合、テクスチャを経由して、つまりまず3Dコンテンツを「フラット化」することが、カスタム3DコンテンツをQt Quick が提供する2D UI要素と統合・混合するための最良の選択肢となります。

import MetalTextureImport
CustomTextureItem {
    id: renderer
    anchors.fill: parent
    anchors.margins: 10

    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
    }

このアプリケーションでは、CustomTextureItemという名前でカスタムQQuickItem サブクラスを公開しています。これはQML内でインスタンス化されます。t プロパティの値もアニメーション化されます。

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

public:
    CustomTextureItem();

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

signals:
    void tChanged();

protected:
    QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;
    void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;

private slots:
    void invalidateSceneGraph();

private:
    void releaseResources() override;

    CustomTextureNode *m_node = nullptr;
    qreal m_t = 0;
};

このカスタムアイテムの実装では、QQuickItem::updatePaintNode() メソッドのオーバーライドに加え、ジオメトリの変更やクリーンアップに関連する関数やスロットのオーバーライドも行います。

class CustomTextureNode : public QSGTextureProvider, public QSGSimpleTextureNode
{
    Q_OBJECT

public:
    CustomTextureNode(QQuickItem *item);
    ~CustomTextureNode();

    QSGTexture *texture() const override;

    void sync();

また、シーングラフノードも必要です。QSGNode を直接継承する代わりに、QSGSimpleTextureNode を使用することで、利便性のため事前に実装済みの機能の一部を利用できます。

QSGNode *CustomTextureItem::updatePaintNode(QSGNode *node, UpdatePaintNodeData *)
{
    CustomTextureNode *n = static_cast<CustomTextureNode *>(node);

    if (!n && (width() <= 0 || height() <= 0))
        return nullptr;

    if (!n) {
        m_node = new CustomTextureNode(this);
        n = m_node;
    }

    m_node->sync();

    n->setTextureCoordinatesTransform(QSGSimpleTextureNode::NoTransform);
    n->setFiltering(QSGTexture::Linear);
    n->setRect(0, 0, width(), height());

    window()->update(); // ensure getting to beforeRendering() at some point

    return n;
}

アイテムの `updatePaintNode()` 関数は、レンダリングスレッド(存在する場合)上で呼び出され、メイン(GUI)スレッドはブロックされます。ここでは、まだノードが存在しない場合は新しいノードを作成し、それを更新します。 ここではメインスレッド上の Qt オブジェクトへのアクセスは安全であるため、sync() はQQuickItem またはQQuickWindow から必要な値を計算してコピーします。

CustomTextureNode::CustomTextureNode(QQuickItem *item)
    : m_item(item)
{
    m_window = m_item->window();
    connect(m_window, &QQuickWindow::beforeRendering, this, &CustomTextureNode::render);
    connect(m_window, &QQuickWindow::screenChanged, this, [this]() {
        if (m_window->effectiveDevicePixelRatio() != m_dpr)
            m_item->update();
    });

このノードは、一般的なQQuickItem -QSGNode という更新シーケンスだけに依存するのではなく、QQuickWindow::beforeRendering() にも接続しています。 そこで、Qt Quick シーングラフのコマンドバッファ上で、そのテクスチャをターゲットとした完全なレンダリングパスをエンコードすることにより、Metal テクスチャの内容が更新されます。beforeRendering() は、Qt Quick が自身のレンダリングコマンドのエンコードを開始する前にこのシグナルが発信されるため、この処理を行うのに適した場所です。 この例では、代わりにQQuickWindow::beforeRenderPassRecording()を選択するとエラーになります。

void CustomTextureNode::sync()
{
    m_dpr = m_window->effectiveDevicePixelRatio();
    const QSize newSize = m_window->size() * m_dpr;
    bool needsNew = false;

    if (!texture())
        needsNew = true;

    if (newSize != m_size) {
        needsNew = true;
        m_size = newSize;
    }

    if (needsNew) {
        delete texture();
        [m_texture release];

        QSGRendererInterface *rif = m_window->rendererInterface();
        m_device = (id<MTLDevice>) rif->getResource(m_window, QSGRendererInterface::DeviceResource);
        Q_ASSERT(m_device);

        MTLTextureDescriptor *desc = [[MTLTextureDescriptor alloc] init];
        desc.textureType = MTLTextureType2D;
        desc.pixelFormat = MTLPixelFormatRGBA8Unorm;
        desc.width = m_size.width();
        desc.height = m_size.height();
        desc.mipmapLevelCount = 1;
        desc.resourceOptions = MTLResourceStorageModePrivate;
        desc.storageMode = MTLStorageModePrivate;
        desc.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget;
        m_texture = [m_device newTextureWithDescriptor: desc];
        [desc release];

        QSGTexture *wrapper = QNativeInterface::QSGMetalTexture::fromNative(m_texture, m_window, m_size);

        qDebug() << "Got QSGTexture wrapper" << wrapper << "for an MTLTexture of size" << m_size;

        setTexture(wrapper);
    }
    m_t = float(static_cast<CustomTextureItem *>(m_item)->t());

必要な値をコピーした後、sync()ではグラフィックスリソースの初期化も実行されます。MTLDeviceはシーングラフから取得されます。MTLTextureが利用可能になると、QNativeInterface::QSGOpenGLTexture::fromNative()を介して、それをラップする(所有しない)QSGTexture が作成されます。最後に、基底クラスのsetTexture()関数を呼び出すことで、QSGTexture が基になるマテリアルに関連付けられます。

void CustomTextureNode::render()
{
    if (!m_initialized)
        return;

    // Render to m_texture.
    MTLRenderPassDescriptor *renderpassdesc = [MTLRenderPassDescriptor renderPassDescriptor];
    MTLClearColor c = MTLClearColorMake(0, 0, 0, 1);
    renderpassdesc.colorAttachments[0].loadAction = MTLLoadActionClear;
    renderpassdesc.colorAttachments[0].storeAction = MTLStoreActionStore;
    renderpassdesc.colorAttachments[0].clearColor = c;
    renderpassdesc.colorAttachments[0].texture = m_texture;

    QSGRendererInterface *rif = m_window->rendererInterface();
    id<MTLCommandBuffer> cb = (id<MTLCommandBuffer>) rif->getResource(m_window, QSGRendererInterface::CommandListResource);
    Q_ASSERT(cb);
    id<MTLRenderCommandEncoder> encoder = [cb renderCommandEncoderWithDescriptor: renderpassdesc];

    const QQuickWindow::GraphicsStateInfo &stateInfo(m_window->graphicsStateInfo());
    void *p = [m_ubuf[stateInfo.currentFrameSlot] contents];
    memcpy(p, &m_t, 4);

    MTLViewport vp;
    vp.originX = 0;
    vp.originY = 0;
    vp.width = m_size.width();
    vp.height = m_size.height();
    vp.znear = 0;
    vp.zfar = 1;
    [encoder setViewport: vp];

    [encoder setFragmentBuffer: m_ubuf[stateInfo.currentFrameSlot] offset: 0 atIndex: 0];
    [encoder setVertexBuffer: m_vbuf offset: 0 atIndex: 1];
    [encoder setRenderPipelineState: m_pipeline];
    [encoder drawPrimitives: MTLPrimitiveTypeTriangleStrip vertexStart: 0 vertexCount: 4 instanceCount: 1 baseInstance: 0];

    [encoder endEncoding];
}

beforeRendering() に接続されたスロットである render() は、sync() で作成されたバッファとパイプライン状態オブジェクトを使用して、レンダリングコマンドをエンコードします。

サンプルプロジェクト @ code.qt.io

© 2026 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.