警告

本节包含从 C++ 自动翻译到 Python 的代码段,可能存在错误。

场景图 - Metal 纹理导入#

展示如何直接使用 Metal 创建纹理。

../_images/metaltextureimport-example.jpg

金属纹理导入示例展示了如何在 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(QQuickItem):

    Q_OBJECT
    Q_PROPERTY(qreal t READ t WRITE setT NOTIFY tChanged)
    QML_ELEMENT
# public
    CustomTextureItem()
    qreal t() { return m_t; }
    def setT(t):
# signals
    def tChanged():
# protected
    QSGNode updatePaintNode(QSGNode , UpdatePaintNodeData ) override
    def geometryChange(newGeometry, oldGeometry):
# private slots
    def invalidateSceneGraph():
# private
    def releaseResources():
    m_node = None
    m_t = 0

我们自定义项目的实现涉及重写 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() 会在 QQuickItemQQuickWindow 中计算并复制所需的值。

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 更新序列,还连接到了 beforeRendering()。正是在这里,通过在 Qt Quicks 场景图的命令缓冲区中编码一个针对纹理的全渲染过程,来更新 Metal 纹理的内容。在 Qt Quick 开始编码它自己的渲染命令之前就发出信号的 beforeRendering() 是做这个的正确地方,因为在选择 beforeRenderPassRecording() 而不是这个例子中的 beforeRendering() 会出错。

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,就通过 QQuickWindow::createTextureFromNativeObject() 创建一个包裹它(但不拥有它)的 QSGTexture 对象。这个函数是 QQuickWindow::createTextureFromId() 的现代等价物,并且与 OpenGL 无关。最后,通过调用基类的 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