Scène graphique - Importation de textures Metal

Ce guide explique comment utiliser une texture créée directement avec Metal.

Squircle converti en texture « Metal » et affiché dans un élément <span translate=Qt Quick" src="images/metaltextureimport-example.jpg" title="Squircle converti en texture « Metal » et affiché dans un élément Qt Quick"/>

L'exemple « Importation de texture Metal » montre comment une application peut importer et utiliser une MTLTexture dans la scène Qt Quick. Cela offre une alternative aux approches par sous-couche (underlay) ou par superposition (overlay) lorsqu'il s'agit d'intégrer le rendu Metal natif. Dans de nombreux cas, passer par une texture, et donc « aplatir » au préalable le contenu 3D, constitue la meilleure option pour intégrer et mélanger du contenu 3D personnalisé avec les éléments d’interface utilisateur 2D fournis par Qt Quick.

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
    }

L’application expose une sous-classe personnalisée de ` QQuickItem ` sous le nom de `CustomTextureItem`. Celle-ci est instanciée en QML. La valeur de la propriété ` t ` est également animée.

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

L’implémentation de notre élément personnalisé implique de redéfinir la méthode ` QQuickItem::updatePaintNode()`, ainsi que les fonctions et les slots liés aux modifications de géométrie et au nettoyage.

class CustomTextureNode : public QSGTextureProvider, public QSGSimpleTextureNode
{
    Q_OBJECT

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

    QSGTexture *texture() const override;

    void sync();

Nous avons également besoin d’un nœud de graphe de scène. Au lieu de dériver directement de ` QSGNode`, nous pouvons utiliser ` QSGSimpleTextureNode `, qui nous offre certaines fonctionnalités pré-implémentées pour plus de commodité.

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

La fonction `updatePaintNode()` de l’élément est appelée sur le thread de rendu (s’il en existe un), le thread principal (GUI) étant bloqué. Ici, nous créons un nouveau nœud s’il n’en existe pas encore, puis nous le mettons à jour. L’accès aux objets Qt résidant sur le thread principal est sans risque ici ; ainsi, la fonction `sync()` calculera et copiera les valeurs dont elle a besoin à partir de ` QQuickItem ` ou ` 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();
    });

Le nœud ne se contente pas de la séquence de mise à jour classique QQuickItem - QSGNode, il s’appuie également sur QQuickWindow::beforeRendering(). C’est là que le contenu de la texture Metal sera mis à jour en encodant un passage de rendu complet, ciblant la texture, dans le tampon de commandes du graphe de scène Qt Quick. La méthode `beforeRendering()` est l’endroit approprié pour cela, car le signal est émis avant que ` Qt Quick ` ne commence à encoder ses propres commandes de rendu. Choisir « QQuickWindow::beforeRenderPassRecording() » à la place serait une erreur dans cet exemple.

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

Après avoir copié les valeurs dont nous avons besoin, sync() effectue également l’initialisation de certaines ressources graphiques. Le MTLDevice est interrogé à partir du graphe de scène. Dès qu’une MTLTexture est disponible, une instance de ` QSGTexture ` l’encapsulant (sans en être propriétaire) est créée via ` QNativeInterface::QSGOpenGLTexture::fromNative()`. Enfin, l’instance de ` QSGTexture ` est associée aux matériaux sous-jacents en appelant la fonction `setTexture()` de la classe parente.

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

La fonction `render()`, le slot connecté à `beforeRendering()`, encode les commandes de rendu à l’aide des tampons et des objets d’état du pipeline créés dans `sync()`.

Exemple de projet sur 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.