Qt Quick 3D - 模板轮廓扩展示例

演示了如何使用 QtQuick3D 渲染扩展来实现模板轮廓。

本例展示了如何使用 QtQuick3D 渲染扩展来支持模板轮廓。

第一步是实现前端物品,通过创建一个新的 渲染扩展 物品以向 QML 公开所需的属性。在此示例中,我们公开了 3 个属性,一个用于绘制轮廓的 target、用于轮廓的 材质,以及用于调整轮廓大小的 scale

class OutlineRenderExtension : public QQuick3DRenderExtension
{
    Q_OBJECT
    Q_PROPERTY(QQuick3DObject * target READ target WRITE setTarget NOTIFY targetChanged)
    Q_PROPERTY(QQuick3DObject * outlineMaterial READ outlineMaterial WRITE setOutlineMaterial NOTIFY outlineMaterialChanged)
    Q_PROPERTY(float outlineScale READ outlineScale WRITE setOutlineScale NOTIFY outlineScaleChanged)
    QML_ELEMENT

public:
    OutlineRenderExtension() = default;
    ~OutlineRenderExtension() override;

    float outlineScale() const;
    void setOutlineScale(float newOutlineScale);

    QQuick3DObject *target() const;
    void setTarget(QQuick3DObject *newTarget);

    QQuick3DObject *outlineMaterial() const;
    void setOutlineMaterial(QQuick3DObject *newOutlineMaterial);

signals:
    void outlineColorChanged();
    void outlineScaleChanged();
    void targetChanged();
    void outlineMaterialChanged();

protected:
    QSSGRenderGraphObject *updateSpatialNode(QSSGRenderGraphObject *node) override;

private:
    enum Dirty : quint8
    {
        Target = 1 << 0,
        OutlineMaterial = 1 << 1,
        OutlineScale = 1 << 2
    };

    using DirtyT = std::underlying_type_t<Dirty>;

    void markDirty(Dirty v);

    QPointer<QQuick3DObject> m_target;
    QPointer<QQuick3DObject> m_outlineMaterial;
    float m_outlineScale = 1.05f;
    DirtyT m_dirtyFlag {};
};

第二步是实现后端 渲染扩展 类,这个类包含 QtQuick3D 将要运行的代码。

对于这个扩展,我们将在内置颜色通过后进行渲染,并将希望作为主渲染过程的一部分进行渲染,因此我们在 QSSGRenderExtension::stage() 和 QSSGRenderExtension::mode() 函数中分别返回 PostColorMain

class OutlineRenderer : public QSSGRenderExtension
{
public:
    OutlineRenderer() = default;

    bool prepareData(QSSGFrameData &data) override;
    void prepareRender(QSSGFrameData &data) override;
    void render(QSSGFrameData &data) override;
    void resetForFrame() override;
    RenderMode mode() const override { return RenderMode::Main; }
    RenderStage stage() const override { return RenderStage::PostColor; };

    QSSGPrepContextId stencilPrepContext { QSSGPrepContextId::Invalid };
    QSSGPrepContextId outlinePrepContext { QSSGPrepContextId::Invalid };
    QSSGPrepResultId stencilPrepResult { QSSGPrepResultId::Invalid };
    QSSGPrepResultId outlinePrepResult { QSSGPrepResultId::Invalid };
    QPointer<QQuick3DObject> model;
    QSSGNodeId modelId { QSSGNodeId::Invalid };
    QPointer<QQuick3DObject> material;
    QSSGResourceId outlineMaterialId {};
    float outlineScale = 1.05f;

    QSSGRenderablesId stencilRenderables;
    QSSGRenderablesId outlineRenderables;
};

接下来需要实现的是 QSSGRenderExtension::prepareData() 函数,此函数应该收集和设置这一扩展将要用于渲染的数据。如果没有要渲染的内容,则此函数应返回 false

bool OutlineRenderer::prepareData(QSSGFrameData &data)
{
    // Make sure we have a model and a material.
    if (!model || !material)
        return false;

    modelId = QQuick3DExtensionHelpers::getNodeId(*model);
    if (modelId == QSSGNodeId::Invalid)
        return false;

    outlineMaterialId = QQuick3DExtensionHelpers::getResourceId(*material);
    if (outlineMaterialId == QSSGResourceId::Invalid)
        return false;

    // This is the active camera for the scene (the camera used to render the QtQuick3D scene)
    QSSGCameraId camera = data.activeCamera();
    if (camera == QSSGCameraId::Invalid)
        return false;

    // We are going to render the same renderable(s) twice so we need to create two contexts.
    stencilPrepContext = QSSGRenderHelpers::prepareForRender(data, *this, camera, 0);
    outlinePrepContext = QSSGRenderHelpers::prepareForRender(data, *this, camera, 1);
    // Create the renderables for the target model. One for the original with stencil write, and one for the outline model.
    // Note that we 'Steal' the model here, that tells QtQuick3D that we'll take over the rendering of the model.
    stencilRenderables = QSSGRenderHelpers::createRenderables(data, stencilPrepContext, { modelId }, QSSGRenderHelpers::CreateFlag::Steal);
    outlineRenderables = QSSGRenderHelpers::createRenderables(data, outlinePrepContext, { modelId });

    // Now we can start setting the data for our models.
    // Here we set a material and a scale for the outline
    QSSGModelHelpers::setModelMaterials(data, outlineRenderables, modelId, { outlineMaterialId });
    QMatrix4x4 globalTransform = QSSGModelHelpers::getGlobalTransform(data, modelId);
    globalTransform.scale(outlineScale);
    QSSGModelHelpers::setGlobalTransform(data, outlineRenderables, modelId, globalTransform);

    // When all changes are done, we need to commit the changes.
    stencilPrepResult = QSSGRenderHelpers::commit(data, stencilPrepContext, stencilRenderables);
    outlinePrepResult = QSSGRenderHelpers::commit(data, outlinePrepContext, outlineRenderables);

    // If there's something to be rendered we return true.
    const bool dataReady = (stencilPrepResult != QSSGPrepResultId::Invalid && outlinePrepResult != QSSGPrepResultId::Invalid);

    return dataReady;
}

如果 QSSGRenderExtension::prepareData() 返回 true,则下一个要调用的函数是 QSSGRenderExtension::prepareRender。在此函数中,我们将为我们的两个可渲染物品设置 管线状态,并通过调用 QSSGRenderHelpers::prepareRenderables() 告诉 QtQuick3D 为可渲染物品准备原语等。

void OutlineRenderer::prepareRender(QSSGFrameData &data)
{
    Q_ASSERT(modelId != QSSGNodeId::Invalid);
    Q_ASSERT(stencilPrepResult != QSSGPrepResultId::Invalid && outlinePrepResult != QSSGPrepResultId::Invalid);

    const auto &ctx = data.contextInterface();

    if (const auto &rhiCtx = ctx->rhiContext()) {
        const QSSGRhiGraphicsPipelineState basePs = data.getPipelineState();
        QRhiRenderPassDescriptor *rpDesc = rhiCtx->mainRenderPassDescriptor();
        const int samples = rhiCtx->mainPassSampleCount();

        { // Original model - Write to the stencil buffer.
            QSSGRhiGraphicsPipelineState ps = basePs;
            ps.flags |= { QSSGRhiGraphicsPipelineState::Flag::BlendEnabled,
                          QSSGRhiGraphicsPipelineState::Flag::DepthWriteEnabled,
                          QSSGRhiGraphicsPipelineState::Flag::UsesStencilRef,
                          QSSGRhiGraphicsPipelineState::Flag::DepthTestEnabled };
            ps.stencilWriteMask = 0xff;
            ps.stencilRef = 1;
            ps.samples = samples;
            ps.cullMode = QRhiGraphicsPipeline::Back;

            ps.stencilOpFrontState = { QRhiGraphicsPipeline::Keep,
                                       QRhiGraphicsPipeline::Keep,
                                       QRhiGraphicsPipeline::Replace,
                                       QRhiGraphicsPipeline::Always };

            QSSGRenderHelpers::prepareRenderables(data, stencilPrepResult, rpDesc, ps);
        }

        { // Scaled version - Only draw outside the original.
            QSSGRhiGraphicsPipelineState ps = basePs;
            ps.flags |= { QSSGRhiGraphicsPipelineState::Flag::BlendEnabled,
                          QSSGRhiGraphicsPipelineState::Flag::UsesStencilRef,
                          QSSGRhiGraphicsPipelineState::Flag::DepthTestEnabled };
            ps.flags.setFlag(QSSGRhiGraphicsPipelineState::Flag::DepthWriteEnabled, false);
            ps.stencilWriteMask = 0;
            ps.stencilRef = 1;
            ps.cullMode = QRhiGraphicsPipeline::Back;

            ps.stencilOpFrontState = { QRhiGraphicsPipeline::Keep,
                                       QRhiGraphicsPipeline::Keep,
                                       QRhiGraphicsPipeline::Replace,
                                       QRhiGraphicsPipeline::NotEqual };

            QSSGRenderHelpers::prepareRenderables(data, outlinePrepResult, rpDesc, ps);
        }
    }
}

当引擎准备记录我们扩展的渲染调用时,它将调用虚拟的 QSSGRenderExtension::render() 函数。在此示例中,我们可以简单地调用 QSSGRenderHelpers::renderRenderables() 对两个模型进行操作,它们将像本地的 QtQuick3D 一样进行渲染,但这一次使用我们的设置。

void OutlineRenderer::render(QSSGFrameData &data)
{
    Q_ASSERT(stencilPrepResult != QSSGPrepResultId::Invalid);

    const auto &ctx = data.contextInterface();
    if (const auto &rhiCtx = ctx->rhiContext()) {
        QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
        cb->debugMarkBegin(QByteArrayLiteral("Stencil outline pass"));
        QSSGRenderHelpers::renderRenderables(data, stencilPrepResult);
        QSSGRenderHelpers::renderRenderables(data, outlinePrepResult);
        cb->debugMarkEnd();
    }
}

通过将 OutlineRenderExtension 添加到 View3Dextensions 属性来激活 OutlineRenderExtension

View3D {
    id: view3d
    anchors.topMargin: 100
    anchors.fill: parent
    extensions: [ OutlineRenderExtension {
            id: outlineRenderer
            outlineMaterial: outlineMaterial
        }
    ]

现在当选中一个 模型 时,我们只需将选中的 模型 设置为 轮廓渲染扩展目标,以便使用轮廓进行渲染。

    MouseArea {
        anchors.fill: view3d
        onClicked: (mouse)=> {
              let hit = view3d.pick(mouse.x, mouse.y)
              outlineRenderer.target = hit.objectHit
        }
    }

文件

图片

© 2024 Qt 公司有限公司。本文件中包含的文档贡献是各自所有者的版权。本文件中的文档是根据自由软件基金会发布的 GNU 自由文档许可协议的条款许可的(版本 1.3)。Qt 及其相关标识是芬兰及/或其他国家的 Qt 公司的商标。所有其他商标均为各自所有者的财产。