Qt Quick 3D - Exemple d'extension pour le tracé au pochoir

Cet exemple montre comment l'extension de rendu « QtQuick3D » peut être utilisée pour implémenter le tracé au pochoir.

Trois modèles représentant une tête de singe, en rose, rouge et vert, avec un contour au pochoir bleu sur le modèle rose

Cet exemple montre comment les extensions de rendu « QtQuick3D » peuvent être utilisées pour ajouter la prise en charge des contours au pochoir.

La première étape consiste à implémenter l’élément frontal en créant un nouvel élément « Render Extension » qui expose les propriétés nécessaires à QML. Dans cet exemple, nous exposons 3 propriétés : une propriété « target » qui prend en paramètre l’ model que nous souhaitons contourner, la propriété « material » que nous souhaitons utiliser pour le contour, et une valeur « scale » permettant d’ajuster la taille du contour.

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

La deuxième étape consiste à implémenter la classe de back-end Render Extension; il s’agit de la classe qui contient le code qui sera exécuté par QtQuick3D.

Pour cette extension, nous allons rendre after le passe de couleur intégré, et nous souhaitons l’intégrer au passe de rendu principal ; nous renverrons donc respectivement PostColor et Main dans nos fonctions QSSGRenderExtension::stage() et QSSGRenderExtension::mode().

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

La fonction suivante à implémenter est QSSGRenderExtension::prepareData(). Cette fonction doit collecter et configurer les données que cette extension utilisera pour le rendu. S’il n’y a rien à rendre, cette fonction doit renvoyer 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;
}

Si QSSGRenderExtension::prepareData() a renvoyé true, la fonction suivante à appeler est QSSGRenderExtension::prepareRender(). Dans cette fonction, nous allons configurer l’ pipeline state e de nos deux éléments à afficher et demander à QtQuick3D de préparer les primitives, etc. pour ces éléments en appelant QSSGRenderHelpers::prepareRenderables().

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

Lorsque le moteur est prêt à enregistrer les appels de rendu pour notre extension, il appelle la fonction virtuelle QSSGRenderExtension::render(). Dans cet exemple, nous pouvons simplement appeler QSSGRenderHelpers::renderRenderables() pour les deux modèles ; ceux-ci seront alors rendus exactement de la même manière que l’aurait fait QtQuick3D en interne, mais cette fois-ci avec nos paramètres.

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

L’ OutlineRenderExtension est activée en l’ajoutant à la propriété View3D's de extensions.

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

Désormais, lorsqu’un modèle « model » est sélectionné, il suffit de définir le modèle « model » sélectionné comme « target » pour le « OutlineRenderExtension » afin qu’il soit affiché avec un contour.

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

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.