Qt Quick 3D - Stencil Outline Extension Example | Qt Quick 3D 6.7.2
Description: Demonstrates how the QtQuick3D Render Extension can be used to implement stencil outlining.

- Qt 6.7
- Qt Quick 3D
- Qt Quick 3D Examples and Tutorials
- Qt Quick 3D - Stencil Outline Extension Example
- Qt Quick 3D | Commercial or GPLv3

# Qt Quick 3D - Stencil Outline Extension Example

     // Copyright (C) 2023 The Qt Company Ltd.
     // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

     #include "outlinerenderextension.h"

     #include <rhi/qrhi.h>

     #include <QtQuick3D/qquick3dobject.h>
     #include <ssg/qquick3dextensionhelpers.h>

     #include <ssg/qssgrenderhelpers.h>
     #include <ssg/qssgrenderextensions.h>
     #include <ssg/qssgrendercontextcore.h>

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

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

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

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

     void OutlineRenderer::resetForFrame()
     {
         stencilPrepContext = { QSSGPrepContextId::Invalid };
         stencilPrepResult = { QSSGPrepResultId::Invalid };
     }

     OutlineRenderExtension::~OutlineRenderExtension() {}

     float OutlineRenderExtension::outlineScale() const
     {
         return m_outlineScale;
     }

     void OutlineRenderExtension::setOutlineScale(float newOutlineScale)
     {
         if (qFuzzyCompare(m_outlineScale, newOutlineScale))
             return;
         m_outlineScale = newOutlineScale;

         markDirty(Dirty::OutlineScale);

         emit outlineScaleChanged();
     }

     QQuick3DObject *OutlineRenderExtension::target() const
     {
         return m_target;
     }

     void OutlineRenderExtension::setTarget(QQuick3DObject *newTarget)
     {
         if (m_target == newTarget)
             return;
         m_target = newTarget;

         markDirty(Dirty::Target);

         emit targetChanged();
     }

     QSSGRenderGraphObject *OutlineRenderExtension::updateSpatialNode(QSSGRenderGraphObject *node)
     {
         if (!node)
             node = new OutlineRenderer;

         OutlineRenderer *renderer = static_cast<OutlineRenderer *>(node);
         renderer->outlineScale = m_outlineScale;
         renderer->model = m_target;
         renderer->material = m_outlineMaterial;

         m_dirtyFlag = {};

         return node;
     }

     void OutlineRenderExtension::markDirty(Dirty v)
     {
         m_dirtyFlag |= v;
         update();
     }

     QQuick3DObject *OutlineRenderExtension::outlineMaterial() const
     {
         return m_outlineMaterial;
     }

     void OutlineRenderExtension::setOutlineMaterial(QQuick3DObject *newOutlineMaterial)
     {
         if (m_outlineMaterial == newOutlineMaterial)
             return;

         m_outlineMaterial = newOutlineMaterial;

         markDirty(Dirty::OutlineMaterial);
         emit outlineMaterialChanged();
     }