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

演示如何使用QtQuick3D 渲染扩展实现模版勾勒。

本例演示了如何使用QtQuick3D 渲染扩展来添加对模版轮廓的支持。

第一步是通过创建一个新的Render Extension 项目来实现前端项目,该项目向 QML 公开所需的属性。在本例中,我们公开了 3 个属性,一个是target ,它包含我们要勾勒的model 、我们要用于勾勒的material ,以及一个用于调整勾勒大小的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 {};
};

第二步是实现后端Render Extension 类,该类包含将由QtQuick3D 运行的代码。

在此扩展中,我们将使用内置的 color-pass 渲染after ,我们希望将渲染作为主渲染传递的一部分,因此我们将在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()。在该函数中,我们将为两个可渲染对象设置pipeline state ,并通过调用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 添加到View3D's extensions 属性中,使其处于活动状态。

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

现在,当选中model 时,我们只需将选中的model 设置为OutlineRenderExtensiontarget ,就可以用轮廓渲染它了。

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

示例项目 @ 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.