Revision control

Copy as Markdown

// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qquick3dviewport_p.h"
#include "qquick3dsceneenvironment_p.h"
#include "qquick3dscenemanager_p.h"
#include "qquick3dtexture_p.h"
#include "qquick3dscenerenderer_p.h"
#include "qquick3dscenerootnode_p.h"
#include "qquick3dcamera_p.h"
#include "qquick3dmodel_p.h"
#include "qquick3drenderstats_p.h"
#include "qquick3ditem2d_p.h"
#include "qquick3ddefaultmaterial_p.h"
#include "qquick3dprincipledmaterial_p.h"
#include "qquick3dcustommaterial_p.h"
#include "qquick3dspecularglossymaterial_p.h"
#include <QtQuick3DRuntimeRender/private/qssgrenderlayer_p.h>
#include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
#include <QtQuick3DUtils/private/qssgassert_p.h>
#include <QtQuick3DUtils/private/qssgfrustum_p.h>
#include <qsgtextureprovider.h>
#include <QSGSimpleTextureNode>
#include <QSGRendererInterface>
#include <QQuickWindow>
#include <QtQuick/private/qquickitem_p.h>
#include <QtQuick/private/qquickpointerhandler_p.h>
#include <QtQml>
#include <QtGui/private/qeventpoint_p.h>
#include <QtCore/private/qnumeric_p.h>
#include <QtCore/qpointer.h>
#include <optional>
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(lcEv, "qt.quick3d.event")
Q_STATIC_LOGGING_CATEGORY(lcPick, "qt.quick3d.pick")
static bool isforceInputHandlingSet()
{
static const bool v = (qEnvironmentVariableIntValue("QT_QUICK3D_FORCE_INPUT_HANDLING") > 0);
return v;
}
struct ViewportTransformHelper : public QQuickDeliveryAgent::Transform
{
static void removeAll() {
for (const auto &o : std::as_const(owners)) {
if (!o.isNull())
o->setSceneTransform(nullptr);
}
owners.clear();
}
void setOnDeliveryAgent(QQuickDeliveryAgent *da) {
da->setSceneTransform(this);
owners.append(da);
}
/*
Transforms viewport coordinates to 2D scene coordinates.
Returns the point in targetItem corresponding to \a viewportPoint,
assuming that targetItem is mapped onto sceneParentNode.
If it's no longer a "hit" on sceneParentNode, returns the last-good point.
*/
QPointF map(const QPointF &viewportPoint) override {
QPointF point = viewportPoint;
// Despite the name, the input coordinates are the window viewport coordinates
// so unless the View3D is the same size of the Window, we need to translate
// to the View3D coordinates before doing any picking.
if (viewport)
point = viewport->mapFromScene(viewportPoint);
point.rx() *= scaleX;
point.ry() *= scaleY;
std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(point);
if (rayResult.has_value()) {
const auto pickResults = renderer->syncPickOne(rayResult.value(), sceneParentNode);
if (!pickResults.isEmpty()) {
const auto pickResult = pickResults.first();
auto ret = pickResult.m_localUVCoords.toPointF();
if (!uvCoordsArePixels) {
ret = QPointF(targetItem->x() + ret.x() * targetItem->width(),
targetItem->y() - ret.y() * targetItem->height() + targetItem->height());
}
const bool outOfModel = pickResult.m_localUVCoords.isNull();
qCDebug(lcEv) << viewportPoint << "->" << (outOfModel ? "OOM" : "") << ret << "@" << pickResult.m_scenePosition
<< "UV" << pickResult.m_localUVCoords << "dist" << qSqrt(pickResult.m_distanceSq);
if (outOfModel) {
return lastGoodMapping;
} else {
lastGoodMapping = ret;
return ret;
}
}
}
return QPointF();
}
QPointer<QQuick3DViewport> viewport;
QQuick3DSceneRenderer *renderer = nullptr;
QSSGRenderNode *sceneParentNode = nullptr;
QPointer<QQuickItem> targetItem;
qreal scaleX = 1;
qreal scaleY = 1;
bool uvCoordsArePixels = false; // if false, they are in the range 0..1
QPointF lastGoodMapping;
static QList<QPointer<QQuickDeliveryAgent>> owners;
};
QList<QPointer<QQuickDeliveryAgent>> ViewportTransformHelper::owners;
class QQuick3DExtensionListHelper
{
Q_DISABLE_COPY_MOVE(QQuick3DExtensionListHelper);
public:
static void extensionAppend(QQmlListProperty<QQuick3DObject> *list, QQuick3DObject *extension)
{
QSSG_ASSERT(list && extension, return);
if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(list->object)) {
if (const auto idx = that->m_extensions.indexOf(extension); idx == -1) {
if (!extension->parentItem())
extension->setParentItem(that->m_sceneRoot);
that->m_extensions.push_back(extension);
that->m_extensionListDirty = true;
}
}
}
static QQuick3DObject *extensionAt(QQmlListProperty<QQuick3DObject> *list, qsizetype index)
{
QQuick3DObject *ret = nullptr;
QSSG_ASSERT(list, return ret);
if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(list->object)) {
if (that->m_extensions.size() > index)
ret = that->m_extensions.at(index);
}
return ret;
}
static qsizetype extensionCount(QQmlListProperty<QQuick3DObject> *list)
{
qsizetype ret = -1;
QSSG_ASSERT(list, return ret);
if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(list->object))
ret = that->m_extensions.size();
return ret;
}
static void extensionClear(QQmlListProperty<QQuick3DObject> *list)
{
QSSG_ASSERT(list, return);
if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(list->object)) {
that->m_extensions.clear();
that->m_extensionListDirty = true;
}
}
static void extensionReplace(QQmlListProperty<QQuick3DObject> *list, qsizetype idx, QQuick3DObject *o)
{
QSSG_ASSERT(list, return);
if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(list->object)) {
if (that->m_extensions.size() > idx && idx > -1) {
that->m_extensions.replace(idx, o);
that->m_extensionListDirty = true;
}
}
}
static void extensionRemoveLast(QQmlListProperty<QQuick3DObject> *list)
{
QSSG_ASSERT(list, return);
if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(list->object)) {
that->m_extensions.removeLast();
that->m_extensionListDirty = true;
}
}
};
/*!
\qmltype View3D
\inherits Item
\inqmlmodule QtQuick3D
\brief Provides a viewport on which to render a 3D scene.
View3D provides a 2D surface on which a 3D scene can be rendered. This
surface is a Qt Quick \l Item and can be placed in a Qt Quick scene.
There are two ways to define the 3D scene that is visualized on the View3D:
If you define a hierarchy of \l{Node}{Node-based} items as children of
the View3D directly, then this will become the implicit scene of the View3D.
It is also possible to reference an existing scene by using the \l importScene
property and setting it to the root \l Node of the scene you want to visualize.
This \l Node does not have to be an ancestor of the View3D, and you can have
multiple View3Ds that import the same scene.
This is demonstrated in \l {Qt Quick 3D - View3D example}{View3D example}.
If the View3D both has child \l{Node}{Nodes} and the \l importScene property is
set simultaneously, then both scenes will be rendered as if they were sibling
subtrees in the same scene.
To control how a scene is rendered, you can set the \l environment
property. The type \l SceneEnvironment has a number of visual properties
that can be adjusted, such as background color, tone mapping, anti-aliasing
and more. \l ExtendedSceneEnvironment in the \c{QtQuick3D.Helpers} module
extends \l SceneEnvironment with even more features, adding common
post-processing effects.
In addition, in order for anything to be rendered in the View3D, the scene
needs a \l Camera. If there is only a single \l Camera in the scene, then
this will automatically be picked. Otherwise, the \l camera property can
be used to select the camera. The \l Camera decides which parts of the scene
are visible, and how they are projected onto the 2D surface.
By default, the 3D scene will first be rendered into an off-screen buffer and
then composited with the rest of the Qt Quick scene when it is done. This provides
the maximum level of compatibility, but may have performance implications on some
graphics hardware. If this is the case, the \l renderMode property can be used to
switch how the View3D is rendered into the window.
A View3D with the default Offscreen \l renderMode is implicitly a
\l{QSGTextureProvider}{texture provider} as well. This means that \l
ShaderEffect or \l{QtQuick3D::Texture::sourceItem}{Texture.sourceItem} can reference
the View3D directly as long as all items live within the same
\l{QQuickWindow}{window}. Like with any other \l Item, it is also possible
to switch the View3D, or one of its ancestors, into a texture-based
\l{QtQuick::Item::layer.enabled}{item layer}.
\sa {Qt Quick 3D - View3D example}
*/
QQuick3DViewport::QQuick3DViewport(QQuickItem *parent)
: QQuickItem(parent)
{
setFlag(ItemHasContents);
m_camera = nullptr;
m_sceneRoot = new QQuick3DSceneRootNode(this);
m_renderStats = new QQuick3DRenderStats();
QQuick3DSceneManager *sceneManager = new QQuick3DSceneManager();
QQuick3DObjectPrivate::get(m_sceneRoot)->refSceneManager(*sceneManager);
Q_ASSERT(sceneManager == QQuick3DObjectPrivate::get(m_sceneRoot)->sceneManager);
connect(sceneManager, &QQuick3DSceneManager::needsUpdate,
this, &QQuickItem::update);
// Overrides the internal input handling to always be true
// instead of potentially updated after a sync (see updatePaintNode)
if (isforceInputHandlingSet()) {
m_enableInputProcessing = true;
updateInputProcessing();
forceActiveFocus();
}
}
QQuick3DViewport::~QQuick3DViewport()
{
// With the threaded render loop m_directRenderer must be destroyed on the
// render thread at the proper time, not here. That's handled in
// releaseResources() + upon sceneGraphInvalidated. However with a
// QQuickRenderControl-based window on the main thread there is a good
// chance that this viewport (and so our sceneGraphInvalidated signal
// connection) is destroyed before the window and the rendercontrol. So act
// here then.
if (m_directRenderer && m_directRenderer->thread() == thread()) {
delete m_directRenderer;
m_directRenderer = nullptr;
}
// If the quick window still exists, make sure to disconnect any of the direct
// connections to this View3D
if (auto qw = window())
disconnect(qw, nullptr, this, nullptr);
auto sceneManager = QQuick3DObjectPrivate::get(m_sceneRoot)->sceneManager;
if (sceneManager) {
sceneManager->setParent(nullptr);
if (auto wa = sceneManager->wattached)
wa->queueForCleanup(sceneManager);
}
delete m_sceneRoot;
m_sceneRoot = nullptr;
delete m_builtInEnvironment;
// m_renderStats is tightly coupled with the render thread, so can't delete while we
// might still be rendering.
m_renderStats->deleteLater();
if (!window() && sceneManager && sceneManager->wattached)
QMetaObject::invokeMethod(sceneManager->wattached, &QQuick3DWindowAttachment::evaluateEol, Qt::QueuedConnection);
}
static void ssgn_append(QQmlListProperty<QObject> *property, QObject *obj)
{
if (!obj)
return;
QQuick3DViewport *view3d = static_cast<QQuick3DViewport *>(property->object);
if (QQuick3DObject *sceneObject = qmlobject_cast<QQuick3DObject *>(obj)) {
QQmlListProperty<QObject> itemProperty = QQuick3DObjectPrivate::get(view3d->scene())->data();
itemProperty.append(&itemProperty, sceneObject);
} else {
QQuickItemPrivate::data_append(property, obj);
}
}
static qsizetype ssgn_count(QQmlListProperty<QObject> *property)
{
QQuick3DViewport *view3d = static_cast<QQuick3DViewport *>(property->object);
if (!view3d || !view3d->scene() || !QQuick3DObjectPrivate::get(view3d->scene())->data().count)
return 0;
QQmlListProperty<QObject> itemProperty = QQuick3DObjectPrivate::get(view3d->scene())->data();
return itemProperty.count(&itemProperty);
}
static QObject *ssgn_at(QQmlListProperty<QObject> *property, qsizetype i)
{
QQuick3DViewport *view3d = static_cast<QQuick3DViewport *>(property->object);
QQmlListProperty<QObject> itemProperty = QQuick3DObjectPrivate::get(view3d->scene())->data();
return itemProperty.at(&itemProperty, i);
}
static void ssgn_clear(QQmlListProperty<QObject> *property)
{
QQuick3DViewport *view3d = static_cast<QQuick3DViewport *>(property->object);
QQmlListProperty<QObject> itemProperty = QQuick3DObjectPrivate::get(view3d->scene())->data();
return itemProperty.clear(&itemProperty);
}
QQmlListProperty<QObject> QQuick3DViewport::data()
{
return QQmlListProperty<QObject>(this,
nullptr,
ssgn_append,
ssgn_count,
ssgn_at,
ssgn_clear);
}
/*!
\qmlproperty QtQuick3D::Camera QtQuick3D::View3D::camera
This property specifies which \l Camera is used to render the scene. If this
property is not set, then the first enabled camera in the scene will be used.
\note It is strongly recommended to set this property explicitly rather than relying on
automatic camera selection. When multiple cameras are present in the scene, automatic
selection does not guarantee which camera will be chosen. When \l{Node::}{layers}
are used, explicitly specifying the camera also avoids unnecessary re-evaluation of
scene nodes.
\note If this property contains a camera that's not \l {Node::visible}{visible} then
no further attempts to find a camera will be done.
\sa PerspectiveCamera, OrthographicCamera, FrustumCamera, CustomCamera
*/
QQuick3DCamera *QQuick3DViewport::camera() const
{
return m_camera;
}
/*!
\qmlproperty QtQuick3D::SceneEnvironment QtQuick3D::View3D::environment
This property specifies the SceneEnvironment used to render the scene.
\note Setting this property to \c null will reset the SceneEnvironment to the default.
\sa SceneEnvironment
*/
QQuick3DSceneEnvironment *QQuick3DViewport::environment() const
{
if (!m_environment) {
if (!m_builtInEnvironment) {
m_builtInEnvironment = new QQuick3DSceneEnvironment;
// Check that we are on the "correct" thread, and move the environment to the
// correct thread if not. This can happen when environment() is called from the
// sync and no scene environment has been set.
if (QThread::currentThread() != m_sceneRoot->thread())
m_builtInEnvironment->moveToThread(m_sceneRoot->thread());
m_builtInEnvironment->setParentItem(m_sceneRoot);
}
return m_builtInEnvironment;
}
return m_environment;
}
/*!
\qmlproperty QtQuick3D::Node QtQuick3D::View3D::scene
\readonly
Returns the root \l Node of the View3D's scene.
To define the 3D scene that is visualized in the View3D:
\list
\li Define a hierarchy of \l{Node}{Node-based} items as children of
the View3D directly, then this will become the implicit \l scene of the
View3D.
\li Reference an existing scene by using the \l importScene property and
set it to the root \l Node of the scene you want to visualize. This
\l Node does not have to be an ancestor of the View3D, and you can have
multiple View3Ds that import the same scene.
\endlist
\sa importScene
*/
QQuick3DNode *QQuick3DViewport::scene() const
{
return m_sceneRoot;
}
/*!
\qmlproperty QtQuick3D::Node QtQuick3D::View3D::importScene
This property defines the reference node of the scene to render to the
viewport. The node does not have to be a child of the View3D. This
referenced node becomes a sibling with child nodes of View3D, if there are
any.
\note Scenes can only be shared between View3D items that are in the same
\l{QQuickWindow}{window}.
\note When sharing scenes between multiple View3D items the imported scene should
be imported in whole, that is, importing a subtree of a scene is not supported.
If multiple View3Ds need to show different parts of the same shared scene,
consider using \l {Node::}{layers} instead.
\note This property can only be set once, and subsequent changes will have
no effect.
You can also define a hierarchy of \l{Node}{Node-based} items as children of
the View3D directly, then this will become the implicit scene of the
View3D.
To return the current scene of the View3D, use the \l scene property.
\sa Node
*/
QQuick3DNode *QQuick3DViewport::importScene() const
{
return m_importScene;
}
/*!
\qmlproperty enumeration QtQuick3D::View3D::renderMode
This property determines how the View3D is combined with the other parts of the
Qt Quick scene.
By default, the scene will be rendered into an off-screen buffer as an intermediate
step. This off-screen buffer is then rendered into the window (or render target) like any
other Qt Quick \l Item.
For most users, there will be no need to change the render mode, and this property can
safely be ignored. But on some graphics hardware, the use of an off-screen buffer can be
a performance bottleneck. If this is the case, it might be worth experimenting with other
modes.
\value View3D.Offscreen The scene is rendered into an off-screen buffer as an intermediate
step. This off-screen buffer is then composited with the rest of the Qt Quick scene.
\value View3D.Underlay The scene is rendered directly to the window before the rest of
the Qt Quick scene is rendered. With this mode, the View3D cannot be placed on top of
other Qt Quick items.
\value View3D.Overlay The scene is rendered directly to the window after Qt Quick is
rendered. With this mode, the View3D will always be on top of other Qt Quick items.
\value View3D.Inline The View3D's scene graph is embedded into the main scene graph,
and the same ordering semantics are applied as to any other Qt Quick \l Item. As this
mode can lead to subtle issues, depending on the contents of the scene, due to
injecting depth-based 3D content into a 2D scene graph, it is not recommended to be
used, unless a specific need arises.
The default is \c{View3D.Offscreen}.
\note When changing the render mode, it is important to note that \c{View3D.Offscreen} (the
default) is the only mode which guarantees perfect graphics fidelity. The other modes
all have limitations that can cause visual glitches, so it is important to check that
the visual output still looks correct when changing this property.
\note When using the Underlay, Overlay, or Inline modes, it can be useful, and in some
cases, required, to disable the Qt Quick scene graph's depth buffer writes via
QQuickGraphicsConfiguration::setDepthBufferFor2D() before showing the QQuickWindow or
QQuickView hosting the View3D item.
*/
QQuick3DViewport::RenderMode QQuick3DViewport::renderMode() const
{
return m_renderMode;
}
/*!
\qmlproperty enumeration QtQuick3D::View3D::renderFormat
\since 6.4
This property determines the backing texture's format. Applicable only when
the View3D is rendering to a texture, for example because the \l renderMode
is \c{View3D.Offscreen}.
The default is \c{ShaderEffectSource.RGBA8}.
If the format is not supported by the underlying graphics driver at run
time, RGBA8 is used.
\list
\li ShaderEffectSource.RGBA8
\li ShaderEffectSource.RGBA16F
\li ShaderEffectSource.RGBA32F
\endlist
\sa QtQuick::ShaderEffectSource::format, QtQuick::Item::layer.format
*/
#if QT_CONFIG(quick_shadereffect)
QQuickShaderEffectSource::Format QQuick3DViewport::renderFormat() const
{
return m_renderFormat;
}
#endif
/*!
\qmlproperty QtQuick3D::RenderStats QtQuick3D::View3D::renderStats
\readonly
This property provides statistics about the rendering of a frame, such as
\l {RenderStats::fps}{fps}, \l {RenderStats::frameTime}{frameTime},
\l {RenderStats::renderTime}{renderTime}, \l {RenderStats::syncTime}{syncTime},
and \l {RenderStats::maxFrameTime}{maxFrameTime}.
*/
QQuick3DRenderStats *QQuick3DViewport::renderStats() const
{
return m_renderStats;
}
QQuick3DSceneRenderer *QQuick3DViewport::createRenderer() const
{
QQuick3DSceneRenderer *renderer = nullptr;
if (QQuickWindow *qw = window()) {
auto wa = QQuick3DSceneManager::getOrSetWindowAttachment(*qw);
auto rci = wa->rci();
if (!rci) {
QSGRendererInterface *rif = qw->rendererInterface();
if (QSSG_GUARD(QSGRendererInterface::isApiRhiBased(rif->graphicsApi()))) {
QRhi *rhi = static_cast<QRhi *>(rif->getResource(qw, QSGRendererInterface::RhiResource));
QSSG_CHECK_X(rhi != nullptr, "No QRhi from QQuickWindow, this cannot happen");
// The RenderContextInterface, and the objects owned by it (such
// as, the BufferManager) are always per-QQuickWindow, and so per
// scenegraph render thread. Hence the association with window.
// Multiple View3Ds in the same window can use the same rendering
// infrastructure (so e.g. the same QSSGBufferManager), but two
// View3D objects in different windows must not, except for certain
// components that do not work with and own native graphics
// resources (most notably, QSSGShaderLibraryManager - but this
// distinction is handled internally by QSSGRenderContextInterface).
rci = std::make_shared<QSSGRenderContextInterface>(rhi);
wa->setRci(rci);
// Use DirectConnection to stay on the render thread, if there is one.
connect(wa, &QQuick3DWindowAttachment::releaseCachedResources, this,
&QQuick3DViewport::onReleaseCachedResources, Qt::DirectConnection);
} else {
qWarning("The Qt Quick scene is using a rendering method that is not based on QRhi and a 3D graphics API. "
"Qt Quick 3D is not functional in such an environment. The View3D item is not going to display anything.");
}
}
if (rci) {
renderer = new QQuick3DSceneRenderer(rci);
Q_QUICK3D_PROFILE_ASSIGN_ID(this, renderer);
}
}
return renderer;
}
bool QQuick3DViewport::isTextureProvider() const
{
// We can only be a texture provider if we are rendering to a texture first
if (m_renderMode == QQuick3DViewport::Offscreen)
return true;
return false;
}
QSGTextureProvider *QQuick3DViewport::textureProvider() const
{
// When Item::layer::enabled == true, QQuickItem will be a texture
// provider. In this case we should prefer to return the layer rather
// than the fbo texture.
if (QQuickItem::isTextureProvider())
return QQuickItem::textureProvider();
// We can only be a texture provider if we are rendering to a texture first
if (m_renderMode != QQuick3DViewport::Offscreen)
return nullptr;
QQuickWindow *w = window();
if (!w) {
qWarning("QSSGView3D::textureProvider: can only be queried on the rendering thread of an exposed window");
return nullptr;
}
if (!m_node)
m_node = new SGFramebufferObjectNode;
return m_node;
}
class CleanupJob : public QRunnable
{
public:
CleanupJob(QQuick3DSGDirectRenderer *renderer) : m_renderer(renderer) { }
void run() override { delete m_renderer; }
private:
QQuick3DSGDirectRenderer *m_renderer;
};
void QQuick3DViewport::releaseResources()
{
if (m_directRenderer) {
window()->scheduleRenderJob(new CleanupJob(m_directRenderer), QQuickWindow::BeforeSynchronizingStage);
m_directRenderer = nullptr;
}
m_node = nullptr;
}
void QQuick3DViewport::cleanupDirectRenderer()
{
delete m_directRenderer;
m_directRenderer = nullptr;
}
void QQuick3DViewport::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
{
QQuickItem::geometryChange(newGeometry, oldGeometry);
if (newGeometry.size() != oldGeometry.size())
update();
}
QSGNode *QQuick3DViewport::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *)
{
// When changing render modes
if (m_renderModeDirty) {
if (node) {
delete node;
node = nullptr;
m_node = nullptr;
m_renderNode = nullptr;
}
if (m_directRenderer) {
delete m_directRenderer;
m_directRenderer = nullptr;
}
}
m_renderModeDirty = false;
switch (m_renderMode) {
// Direct rendering
case Underlay:
Q_FALLTHROUGH();
case Overlay:
setupDirectRenderer(m_renderMode);
node = nullptr;
break;
case Offscreen:
node = setupOffscreenRenderer(node);
break;
case Inline:
// QSGRenderNode-based rendering
node = setupInlineRenderer(node);
break;
}
if (!isforceInputHandlingSet()) {
// Implicitly enable internal input processing if any item2ds are present.
const auto inputHandlingEnabled =
QQuick3DObjectPrivate::get(m_sceneRoot)->sceneManager->inputHandlingEnabled;
const auto enable = inputHandlingEnabled > 0;
if (m_enableInputProcessing != enable) {
m_enableInputProcessing = enable;
QMetaObject::invokeMethod(this, "updateInputProcessing", Qt::QueuedConnection);
}
}
return node;
}
void QQuick3DViewport::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
{
if (change == ItemSceneChange) {
if (value.window) {
// TODO: if we want to support multiple windows, there has to be a scene manager for
// every window.
QQuick3DObjectPrivate::get(m_sceneRoot)->sceneManager->setWindow(value.window);
if (m_importScene)
QQuick3DObjectPrivate::get(m_importScene)->sceneManager->setWindow(value.window);
m_renderStats->setWindow(value.window);
}
} else if (change == ItemVisibleHasChanged && isVisible()) {
m_visibilityChanged = true;
update();
}
}
bool QQuick3DViewport::event(QEvent *event)
{
if (m_enableInputProcessing && event->isPointerEvent())
return internalPick(static_cast<QPointerEvent *>(event));
else
return QQuickItem::event(event);
}
void QQuick3DViewport::componentComplete()
{
QQuickItem::componentComplete();
Q_QUICK3D_PROFILE_REGISTER(this);
}
void QQuick3DViewport::setCamera(QQuick3DCamera *camera)
{
if (m_camera == camera)
return;
if (camera && !camera->parentItem())
camera->setParentItem(m_sceneRoot);
if (camera)
camera->updateGlobalVariables(QRect(0, 0, width(), height()));
QQuick3DObjectPrivate::attachWatcherPriv(m_sceneRoot, this, &QQuick3DViewport::setCamera, camera, m_camera);
m_camera = camera;
emit cameraChanged();
update();
}
void QQuick3DViewport::setMultiViewCameras(QQuick3DCamera **firstCamera, int count)
{
m_multiViewCameras.clear();
bool sendChangeSignal = false;
for (int i = 0; i < count; ++i) {
QQuick3DCamera *camera = *(firstCamera + i);
if (camera) {
if (!camera->parentItem())
camera->setParentItem(m_sceneRoot);
camera->updateGlobalVariables(QRect(0, 0, width(), height()));
}
if (i == 0) {
if (m_camera != camera) {
m_camera = camera;
sendChangeSignal = true;
}
}
m_multiViewCameras.append(camera);
// ### do we need attachWatcher stuff? the Xr-provided cameras cannot disappear, although the XrActor (the owner) might
}
if (sendChangeSignal)
emit cameraChanged();
update();
}
void QQuick3DViewport::setEnvironment(QQuick3DSceneEnvironment *environment)
{
if (m_environment == environment)
return;
m_environment = environment;
if (m_environment && !m_environment->parentItem())
m_environment->setParentItem(m_sceneRoot);
QQuick3DObjectPrivate::attachWatcherPriv(m_sceneRoot, this, &QQuick3DViewport::setEnvironment, environment, m_environment);
emit environmentChanged();
update();
}
void QQuick3DViewport::setImportScene(QQuick3DNode *inScene)
{
// ### We may need consider the case where there is
// already a scene tree here
// FIXME : Only the first importScene is an effective one
if (m_importScene)
return;
// FIXME : Check self-import or cross-import
// Currently it does not work since importScene qml parsed in a reverse order.
QQuick3DNode *scene = inScene;
while (scene) {
if (m_sceneRoot == scene) {
qmlWarning(this) << "Cannot allow self-import or cross-import!";
return;
}
QQuick3DSceneRootNode *rn = qobject_cast<QQuick3DSceneRootNode *>(scene);
scene = rn ? rn->view3D()->importScene() : nullptr;
}
m_importScene = inScene;
if (m_importScene)
updateSceneManagerForImportScene();
emit importSceneChanged();
update();
}
void QQuick3DViewport::setRenderMode(QQuick3DViewport::RenderMode renderMode)
{
if (m_renderMode == renderMode)
return;
m_renderMode = renderMode;
m_renderModeDirty = true;
emit renderModeChanged();
update();
}
#if QT_CONFIG(quick_shadereffect)
void QQuick3DViewport::setRenderFormat(QQuickShaderEffectSource::Format format)
{
if (m_renderFormat == format)
return;
m_renderFormat = format;
m_renderModeDirty = true;
emit renderFormatChanged();
update();
}
#endif
/*!
\qmlproperty int QtQuick3D::View3D::explicitTextureWidth
\since 6.7
The width, in pixels, of the item's associated texture. Relevant when a
fixed texture size is desired that does not depend on the item's size. This
size has no effect on the geometry of the item (its size and placement
within the scene), which means the texture's content will appear scaled up
or down (and possibly stretched) onto the item's area.
By default the value is \c 0. A value of 0 means that texture's size
follows the item's size. (\c{texture size in pixels} = \c{item's logical
size} * \c{device pixel ratio}).
\note This property is relevant only when \l renderMode is set to \c
Offscreen. Its value is ignored otherwise.
\sa explicitTextureHeight, effectiveTextureSize, DebugView
*/
int QQuick3DViewport::explicitTextureWidth() const
{
return m_explicitTextureWidth;
}
void QQuick3DViewport::setExplicitTextureWidth(int width)
{
if (m_explicitTextureWidth == width)
return;
m_explicitTextureWidth = width;
emit explicitTextureWidthChanged();
update();
}
/*!
\qmlproperty int QtQuick3D::View3D::explicitTextureHeight
\since 6.7
The height, in pixels, of the item's associated texture. Relevant when a
fixed texture size is desired that does not depend on the item's size. This
size has no effect on the geometry of the item (its size and placement
within the scene), which means the texture's content will appear scaled up
or down (and possibly stretched) onto the item's area.
By default the value is \c 0. A value of 0 means that texture's size
follows the item's size. (\c{texture size in pixels} = \c{item's logical
size} * \c{device pixel ratio}).
\note This property is relevant only when \l renderMode is set to \c
Offscreen. Its value is ignored otherwise.
\sa explicitTextureWidth, effectiveTextureSize, DebugView
*/
int QQuick3DViewport::explicitTextureHeight() const
{
return m_explicitTextureHeight;
}
void QQuick3DViewport::setExplicitTextureHeight(int height)
{
if (m_explicitTextureHeight == height)
return;
m_explicitTextureHeight = height;
emit explicitTextureHeightChanged();
update();
}
/*!
\qmlproperty size QtQuick3D::View3D::effectiveTextureSize
\since 6.7
This property exposes the size, in pixels, of the underlying color (and
depth/stencil) buffers. It is provided for use on the GUI (main) thread, in
QML bindings or JavaScript.
This is a read-only property.
\note This property is relevant only when \l renderMode is set to
\c Offscreen.
\sa explicitTextureWidth, explicitTextureHeight, DebugView
*/
QSize QQuick3DViewport::effectiveTextureSize() const
{
return m_effectiveTextureSize;
}
/*!
\qmlmethod vector3d View3D::mapFrom3DScene(vector3d scenePos)
Transforms \a scenePos from scene space (3D) into view space (2D).
The returned x- and y-values will be be in view coordinates, with the top-left
corner at [0, 0] and the bottom-right corner at [width, height]. The returned
z-value contains the distance from the near clip plane of the frustum (clipNear) to
\a scenePos in scene coordinates. If the distance is negative, that means the \a scenePos
is behind the active camera. If \a scenePos cannot be mapped to a position in the scene,
a position of [0, 0, 0] is returned.
This function requires that \l camera is assigned to the view.
\sa mapTo3DScene(), {Camera::mapToViewport()}{Camera.mapToViewport()}
*/
QVector3D QQuick3DViewport::mapFrom3DScene(const QVector3D &scenePos) const
{
if (!m_camera) {
qmlWarning(this) << "Cannot resolve view position without a camera assigned!";
return QVector3D(0, 0, 0);
}
qreal _width = width();
qreal _height = height();
if (_width == 0 || _height == 0)
return QVector3D(0, 0, 0);
const QVector3D normalizedPos = m_camera->mapToViewport(scenePos, _width, _height);
return normalizedPos * QVector3D(float(_width), float(_height), 1);
}
/*!
\qmlmethod vector3d View3D::mapTo3DScene(vector3d viewPos)
Transforms \a viewPos from view space (2D) into scene space (3D).
The x- and y-values of \a viewPos should be in view coordinates, with the top-left
corner at [0, 0] and the bottom-right corner at [width, height]. The z-value is
interpreted as the distance from the near clip plane of the frustum (clipNear) in
scene coordinates.
If \a viewPos cannot successfully be mapped to a position in the scene, a position of
[0, 0, 0] is returned.
This function requires that a \l camera is assigned to the view.
\sa mapFrom3DScene(), {Camera::mapFromViewport}{Camera.mapFromViewport()}
*/
QVector3D QQuick3DViewport::mapTo3DScene(const QVector3D &viewPos) const
{
if (!m_camera) {
qmlWarning(this) << "Cannot resolve scene position without a camera assigned!";
return QVector3D(0, 0, 0);
}
qreal _width = width();
qreal _height = height();
if (_width == 0 || _height == 0)
return QVector3D(0, 0, 0);
const QVector3D normalizedPos = viewPos / QVector3D(float(_width), float(_height), 1);
return m_camera->mapFromViewport(normalizedPos, _width, _height);
}
/*!
\qmlmethod pickResult View3D::pick(float x, float y)
This method will "shoot" a ray into the scene from view coordinates \a x and \a y
and return information about the nearest intersection with an object in the scene.
This can, for instance, be called with mouse coordinates to find the object under the mouse cursor.
Triangles that the material's \l {Material::cullMode}{cull mode} discards
when rendering are not picked either, so only geometry that is actually
visible along the ray is reported.
\sa pickAll(), pickSubset(), pickInRect()
*/
QQuick3DPickResult QQuick3DViewport::pick(float x, float y) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return QQuick3DPickResult();
const QPointF position(qreal(x) * window()->effectiveDevicePixelRatio() * m_widthMultiplier,
qreal(y) * window()->effectiveDevicePixelRatio() * m_heightMultiplier);
std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(position);
if (!rayResult.has_value())
return QQuick3DPickResult();
const auto resultList = renderer->syncPick(rayResult.value());
return getNearestPickResult(resultList);
}
/*!
\qmlmethod pickResult View3D::pick(float x, float y, Model model)
This method will "shoot" a ray into the scene from view coordinates \a x and \a y
and return information about the intersection between the ray and the specified \a model.
This can, for instance, be called with mouse coordinates to find the object under the mouse cursor.
\since 6.8
\sa pickAll(), pickSubset(), pickInRect()
*/
QQuick3DPickResult QQuick3DViewport::pick(float x, float y, QQuick3DModel *model) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return QQuick3DPickResult();
const QPointF position(qreal(x) * window()->effectiveDevicePixelRatio(),
qreal(y) * window()->effectiveDevicePixelRatio());
std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(position);
if (!rayResult.has_value())
return QQuick3DPickResult();
const auto renderNode = static_cast<QSSGRenderNode *>(QQuick3DObjectPrivate::get(model)->spatialNode);
const auto resultList = renderer->syncPickOne(rayResult.value(), renderNode);
return getNearestPickResult(resultList);
}
/*!
\qmlmethod List<pickResult> View3D::pickSubset(float x, float y, list<Model> models)
This method will "shoot" a ray into the scene from view coordinates \a x and \a y
and return information about the intersections with the passed in list of \a models.
This will only check against the list of models passed in.
The returned list is sorted by distance from the camera with the nearest
intersections appearing first and the furthest appearing last.
This can, for instance, be called with mouse coordinates to find the object under the mouse cursor.
Works with both property list<Model> and dynamic JavaScript arrays of models.
\since 6.8
\sa pick(), pickAll(), pickInRect()
*/
QList<QQuick3DPickResult> QQuick3DViewport::pickSubset(float x, float y, const QJSValue &models) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return {};
QVarLengthArray<QSSGRenderNode*> renderNodes;
// Check for regular JavaScript array
if (models.isArray()) {
const auto length = models.property(QStringLiteral("length")).toInt();
if (length == 0)
return {};
for (int i = 0; i < length; ++i) {
const auto isQObject = models.property(i).isQObject();
if (!isQObject) {
qmlWarning(this) << "Type provided for picking is not a QObject. Needs to be of type QQuick3DModel.";
continue;
}
const auto obj = models.property(i).toQObject();
const auto model = qobject_cast<QQuick3DModel *>(obj);
if (!model) {
qmlWarning(this) << "Type " << obj->metaObject()->className() << " is not supported for picking. Needs to be of type QQuick3DModel.";
continue;
}
const auto priv = QQuick3DObjectPrivate::get(model);
if (priv && priv->spatialNode) {
renderNodes.push_back(static_cast<QSSGRenderNode*>(priv->spatialNode));
}
}
} else {
// Check for property list<Model>
const auto subsetVariant = models.toVariant();
if (!subsetVariant.isValid() || !subsetVariant.canConvert<QQmlListReference>())
return {};
const auto list = subsetVariant.value<QQmlListReference>();
// Only support array of models
if (list.listElementType()->className() != QQuick3DModel::staticMetaObject.className()) {
qmlWarning(this) << "Type " << list.listElementType()->className() << " is not supported for picking. Needs to be of type QQuick3DModel.";
return {};
}
for (int i = 0; i < list.count(); ++i) {
auto model = static_cast<QQuick3DModel *>(list.at(i));
if (!model)
continue;
auto priv = QQuick3DObjectPrivate::get(model);
if (priv && priv->spatialNode) {
renderNodes.push_back(static_cast<QSSGRenderNode*>(priv->spatialNode));
}
}
}
if (renderNodes.empty())
return {};
const QPointF position(qreal(x) * window()->effectiveDevicePixelRatio(),
qreal(y) * window()->effectiveDevicePixelRatio());
std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(position);
if (!rayResult.has_value())
return {};
const auto resultList = renderer->syncPickSubset(rayResult.value(), renderNodes);
QList<QQuick3DPickResult> processedResultList;
processedResultList.reserve(resultList.size());
for (const auto &result : resultList)
processedResultList.append(processPickResult(result));
return processedResultList;
}
/*!
\qmlmethod List<pickResult> View3D::pickAll(float x, float y)
This method will "shoot" a ray into the scene from view coordinates \a x and \a y
and return a list of information about intersections with objects in the scene.
The returned list is sorted by distance from the camera with the nearest
intersections appearing first and the furthest appearing last.
This can, for instance, be called with mouse coordinates to find the object under the mouse cursor.
\since 6.2
\sa pick(), pickSubset(), pickInRect()
*/
QList<QQuick3DPickResult> QQuick3DViewport::pickAll(float x, float y) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return QList<QQuick3DPickResult>();
const QPointF position(qreal(x) * window()->effectiveDevicePixelRatio() * m_widthMultiplier,
qreal(y) * window()->effectiveDevicePixelRatio() * m_heightMultiplier);
std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(position);
if (!rayResult.has_value())
return QList<QQuick3DPickResult>();
const auto resultList = renderer->syncPickAll(rayResult.value());
QList<QQuick3DPickResult> processedResultList;
processedResultList.reserve(resultList.size());
for (const auto &result : resultList)
processedResultList.append(processPickResult(result));
return processedResultList;
}
/*!
\qmlmethod pickResult View3D::rayPick(vector3d origin, vector3d direction)
This method will "shoot" a ray into the scene starting at \a origin and in
\a direction and return information about the nearest intersection with an
object in the scene.
This can, for instance, be called with the position and forward vector of
any object in a scene to see what object is in front of an item. This
makes it possible to do picking from any point in the scene.
Triangles that the material's \l {Material::cullMode}{cull mode} discards
when rendering are not picked either, so only geometry that is actually
visible along the ray is reported.
\since 6.2
*/
QQuick3DPickResult QQuick3DViewport::rayPick(const QVector3D &origin, const QVector3D &direction) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return QQuick3DPickResult();
const QSSGRenderRay ray(origin, direction);
const auto resultList = renderer->syncPick(ray);
return getNearestPickResult(resultList);
}
/*!
\qmlmethod List<pickResult> View3D::rayPickAll(vector3d origin, vector3d direction)
This method will "shoot" a ray into the scene starting at \a origin and in
\a direction and return a list of information about the nearest intersections with
objects in the scene.
The list is presorted by distance from the origin along the direction
vector with the nearest intersections appearing first and the furthest
appearing last.
This can, for instance, be called with the position and forward vector of
any object in a scene to see what objects are in front of an item. This
makes it possible to do picking from any point in the scene.
\since 6.2
*/
QList<QQuick3DPickResult> QQuick3DViewport::rayPickAll(const QVector3D &origin, const QVector3D &direction) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return QList<QQuick3DPickResult>();
const QSSGRenderRay ray(origin, direction);
const auto resultList = renderer->syncPickAll(ray);
QList<QQuick3DPickResult> processedResultList;
processedResultList.reserve(resultList.size());
for (const auto &result : resultList) {
auto processedResult = processPickResult(result);
if (processedResult.hitType() != QQuick3DPickResultEnums::HitType::Null)
processedResultList.append(processedResult);
}
return processedResultList;
}
/*!
\qmlmethod pickResult View3D::rayPick(vector3d origin, vector3d direction, Model model)
This method will "shoot" a ray into the scene starting at \a origin and in
\a direction and return information about the intersection between the ray and the specified \a model.
\since 6.11
*/
QQuick3DPickResult QQuick3DViewport::rayPick(const QVector3D &origin, const QVector3D &direction, QQuick3DModel *model) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return QQuick3DPickResult();
const QSSGRenderRay ray(origin, direction);
const auto renderNode = static_cast<QSSGRenderNode *>(QQuick3DObjectPrivate::get(model)->spatialNode);
const auto resultList = renderer->syncPickOne(ray, renderNode);
return getNearestPickResult(resultList);
}
/*!
\qmlmethod pickResult View3D::closestPointPick(vector3d origin, float radius, Model model)
This method will find the point on the surface of \a model that is nearest to \a origin, within a distance of
\a radius. If \a model is \c null, the closest object within \a radius will be found.
If no such object exists, \c null is returned.
\since 6.11
*/
QQuick3DPickResult QQuick3DViewport::closestPointPick(const QVector3D &origin, float radius, QQuick3DModel *model) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return QQuick3DPickResult();
QSSGRenderNode *renderNode = nullptr;
if (model) {
renderNode = static_cast<QSSGRenderNode *>(QQuick3DObjectPrivate::get(model)->spatialNode);
if (Q_UNLIKELY(!renderNode))
return QQuick3DPickResult{};
}
const auto pickResult = renderer->syncPickClosestPoint(origin, radius * radius, renderNode);
if (!pickResult.has_value())
return QQuick3DPickResult{};
return processPickResult(pickResult.value());
}
/*!
\qmlmethod list<Object3D> View3D::pickInRect(point start, point end)
\since 6.11
This method picks all objects within a rectangular region defined by
\a start and \a end points in view coordinates.
Returns a list of objects that are within the specified rectangle. This
can be used for marquee selection, where the user drags a rectangle to
select multiple objects.
\sa pick(), pickAll(), pickSubset()
*/
QList<QQuick3DObject *> QQuick3DViewport::pickInRect(const QPointF &start, const QPointF &end) const
{
const qreal minX = qMin(start.x(), end.x());
const qreal maxX = qMax(start.x(), end.x());
const qreal minY = qMin(start.y(), end.y());
const qreal maxY = qMax(start.y(), end.y());
const qreal ndc[4] = {
2.0f * minX / width() - 1.0f, // left
2.0f * maxX / width() - 1.0f, // right
// flip Y-coordinates
1.0f - 2.0f * maxY / height(), // bottom
1.0f - 2.0f * minY / height(), // top
};
const float near = 0.0f;
const float far = 1.0f;
enum { L, R, B, T }; // left, right, bottom, top
const QVector4D ndcCorners[8] = {
// Near plane
QVector4D(ndc[L], ndc[B], near, 1.0f), // 0: bottom-left | n0
QVector4D(ndc[R], ndc[B], near, 1.0f), // 1: bottom-right | n1
QVector4D(ndc[L], ndc[T], near, 1.0f), // 3: top-left | n2
QVector4D(ndc[R], ndc[T], near, 1.0f), // 2: top-right | n3
// Far plane
QVector4D(ndc[L], ndc[B], far, 1.0f), // 4: bottom-left | f0
QVector4D(ndc[R], ndc[B], far, 1.0f), // 5: bottom-right | f1
QVector4D(ndc[L], ndc[T], far, 1.0f), // 7: top-left | f2
QVector4D(ndc[R], ndc[T], far, 1.0f), // 6: top-right | f3
};
QMatrix4x4 viewProjection;
if (this->camera()) {
if (auto camera = static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(this->camera())->spatialNode))
camera->calculateViewProjectionMatrix(camera->localTransform, camera->projection, viewProjection);
}
QMatrix4x4 viewProjectionInverted = viewProjection.inverted();
QVector3D worldCorners[8];
for (int i = 0; i < 8; ++i) {
QVector4D worldPoint = viewProjectionInverted * ndcCorners[i];
worldCorners[i] = worldPoint.toVector3D() / worldPoint.w(); // perspective divide
}
// Frustum
//
// f2 ------------f3
// | |
// | |
// | |
// | |
// f0 ------------f1
//
// n2 -------- n3
// | |
// | camera |
// | |
// n0 -------- n1
//
// CCW winding if inside the frustum and looking toward the face
const QSSGFrustum frustum {
QSSGPlane(worldCorners[0], worldCorners[4], worldCorners[6]), // L [n0, f0, f2]
QSSGPlane(worldCorners[1], worldCorners[3], worldCorners[7]), // R [n1, n3, f3]
QSSGPlane(worldCorners[0], worldCorners[1], worldCorners[5]), // B [n0, n1, f1]
QSSGPlane(worldCorners[2], worldCorners[6], worldCorners[7]), // T [n2, f2, f3]
QSSGPlane(worldCorners[0], worldCorners[2], worldCorners[3]), // N [n0, n2, n3]
QSSGPlane(worldCorners[5], worldCorners[7], worldCorners[6]), // F [f1, f3, f2]
};
QList<QQuick3DObject *> ret;
if (QQuick3DSceneRenderer *renderer = getRenderer()) {
auto nodes = renderer->syncPickInFrustum(frustum);
for (const auto *node : std::as_const(nodes)) {
if (QQuick3DObject *m = findFrontendNode(node))
ret.append(m);
}
}
return ret;
}
void QQuick3DViewport::processPointerEventFromRay(const QVector3D &origin, const QVector3D &direction, QPointerEvent *event) const
{
internalPick(event, origin, direction);
}
// Note: we have enough information to implement Capability::Hover and Capability::ZPosition,
// but those properties are not currently available in QTouchEvent/QEventPoint
namespace {
class SyntheticTouchDevice : public QPointingDevice
{
public:
SyntheticTouchDevice(QObject *parent = nullptr)
: QPointingDevice(QLatin1StringView("QtQuick3D Touch Synthesizer"),
0,
DeviceType::TouchScreen,
PointerType::Finger,
Capability::Position,
10, 0,
QString(), QPointingDeviceUniqueId(),
parent)
{
}
};
}
/*!
\qmlmethod void View3D::setTouchpoint(Item target, point position, int pointId, bool pressed)
Sends a synthetic touch event to \a target, moving the touch point with ID \a pointId to \a position,
with \a pressed determining if the point is pressed.
Also sends the appropriate touch release event if \a pointId was previously active on a different
item.
\since 6.8
*/
void QQuick3DViewport::setTouchpoint(QQuickItem *target, const QPointF &position, int pointId, bool pressed)
{
if (pointId >= m_touchState.size())
m_touchState.resize(pointId + 1);
auto prevState = m_touchState[pointId];
const bool sameTarget = prevState.target == target;
const bool wasPressed = prevState.isPressed;
const bool isPress = pressed && (!sameTarget || !wasPressed);
const bool isRelease = !pressed && wasPressed && sameTarget;
// Hover if we're not active, and we weren't previously active.
// We assume that we always get a non-active for a target when we release.
// This function sends a release events if the target is changed.
if (!sameTarget && wasPressed)
qWarning("QQuick3DViewport::setTouchpoint missing release event");
if (!pressed && !wasPressed) {
// This would be a hover event: skipping
return;
}
m_touchState[pointId] = { target, position, pressed };
if (!m_syntheticTouchDevice)
m_syntheticTouchDevice = new SyntheticTouchDevice(this);
QPointingDevicePrivate *devPriv = QPointingDevicePrivate::get(m_syntheticTouchDevice);
auto makePoint = [devPriv](int id, QEventPoint::State pointState, QPointF pos, quint64 timestamp) -> QEventPoint {
auto epd = devPriv->pointById(id);
auto &ep = epd->eventPoint;
if (pointState != QEventPoint::State::Stationary)
ep.setAccepted(false);
auto res = QMutableEventPoint::withTimeStamp(timestamp, id, pointState, pos, pos, pos);
QMutableEventPoint::update(res, ep);
if (pointState == QEventPoint::State::Pressed)
QMutableEventPoint::setGlobalPressPosition(res, pos);
else if (ep.state() != QEventPoint::State::Unknown)
QMutableEventPoint::setGlobalPressPosition(res, ep.globalPressPosition());
return res;
};
auto sendTouchEvent = [&](QQuickItem *t, const QPointF &position, int pointId, QEventPoint::State pointState, quint64 timestamp) -> void {
QList<QEventPoint> points;
bool otherPoint = false; // Does the event have another point already?
for (int i = 0; i < m_touchState.size(); ++i) {
const auto &ts = m_touchState[i];
if (ts.target != t)
continue;
if (i == pointId) {
auto newPoint = makePoint(i, pointState, position, timestamp);
points << newPoint;
} else if (ts.isPressed) {
otherPoint = true;
points << makePoint(i, QEventPoint::Stationary, ts.position, timestamp);
}
}
QEvent::Type type;
if (pointState == QEventPoint::Pressed && !otherPoint)
type = QEvent::Type::TouchBegin;
else if (pointState == QEventPoint::Released && !otherPoint)
type = QEvent::Type::TouchEnd;
else
type = QEvent::Type::TouchUpdate;
QTouchEvent ev(type, m_syntheticTouchDevice, {}, points);
ev.setTimestamp(timestamp);
if (t) {
// Actually send event:
auto da = QQuickItemPrivate::get(t)->deliveryAgent();
bool handled = da->event(&ev);
Q_UNUSED(handled);
}
// Duplicate logic from QQuickWindowPrivate::clearGrabbers
if (ev.isEndEvent()) {
for (auto &point : ev.points()) {
if (point.state() == QEventPoint::State::Released) {
ev.setExclusiveGrabber(point, nullptr);
ev.clearPassiveGrabbers(point);
}
}
}
};
auto timestamp = QDateTime::currentMSecsSinceEpoch();
// Send a release event to the previous target
if (prevState.target && !sameTarget)
sendTouchEvent(prevState.target, prevState.position, pointId, QEventPoint::Released, timestamp);
// Now send an event for the new state
QEventPoint::State newState = isPress ? QEventPoint::Pressed : isRelease ? QEventPoint::Released : QEventPoint::Updated;
sendTouchEvent(target, position, pointId, newState, timestamp);
}
QQuick3DLightmapBaker *QQuick3DViewport::maybeLightmapBaker()
{
return m_lightmapBaker;
}
QQuick3DLightmapBaker *QQuick3DViewport::lightmapBaker()
{
if (!m_lightmapBaker)
m_lightmapBaker= new QQuick3DLightmapBaker(this);
return m_lightmapBaker;
}
/*!
\internal
*/
void QQuick3DViewport::bakeLightmap()
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer || !renderer->m_layer->renderData)
return;
const bool currentlyBaking = renderer->m_layer->renderData->lightmapBaker != nullptr;
if (!currentlyBaking)
lightmapBaker()->bake();
}
/*!
\internal
*/
void QQuick3DViewport::denoiseLightmap()
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer || !renderer->m_layer->renderData)
return;
const bool currentlyBaking = renderer->m_layer->renderData->lightmapBaker != nullptr;
if (!currentlyBaking)
lightmapBaker()->denoise();
}
void QQuick3DViewport::setGlobalPickingEnabled(bool isEnabled)
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer)
return;
renderer->setGlobalPickingEnabled(isEnabled);
}
void QQuick3DViewport::invalidateSceneGraph()
{
m_node = nullptr;
m_renderNode = nullptr;
}
QQuick3DSceneRenderer *QQuick3DViewport::getRenderer() const
{
QQuick3DSceneRenderer *renderer = nullptr;
if (m_node) {
renderer = m_node->renderer;
} else if (m_renderNode) {
renderer = m_renderNode->renderer;
} else if (m_directRenderer) {
renderer = m_directRenderer->renderer();
}
return renderer;
}
void QQuick3DViewport::updateDynamicTextures()
{
// Update QSGDynamicTextures that are used for source textures and Quick items
// Must be called on the render thread.
const auto &sceneManager = QQuick3DObjectPrivate::get(m_sceneRoot)->sceneManager;
for (auto *texture : std::as_const(sceneManager->qsgDynamicTextures))
texture->updateTexture();
QQuick3DNode *scene = m_importScene;
while (scene) {
const auto &importSm = QQuick3DObjectPrivate::get(scene)->sceneManager;
if (importSm != sceneManager) {
for (auto *texture : std::as_const(importSm->qsgDynamicTextures))
texture->updateTexture();
}
// if importScene has another import
QQuick3DSceneRootNode *rn = qobject_cast<QQuick3DSceneRootNode *>(scene);
scene = rn ? rn->view3D()->importScene() : nullptr;
}
}
QSGNode *QQuick3DViewport::setupOffscreenRenderer(QSGNode *node)
{
SGFramebufferObjectNode *n = static_cast<SGFramebufferObjectNode *>(node);
if (!n) {
if (!m_node)
m_node = new SGFramebufferObjectNode;
n = m_node;
}
if (!n->renderer) {
n->window = window();
n->renderer = createRenderer();
if (!n->renderer)
return nullptr;
n->renderer->fboNode = n;
n->quickFbo = this;
connect(window(), SIGNAL(screenChanged(QScreen*)), n, SLOT(handleScreenChange()));
}
const qreal dpr = window()->effectiveDevicePixelRatio();
const QSize minFboSize = QQuickItemPrivate::get(this)->sceneGraphContext()->minimumFBOSize();
QSize desiredFboSize = QSize(m_explicitTextureWidth, m_explicitTextureHeight);
if (desiredFboSize.isEmpty()) {
desiredFboSize = QSize(width(), height()) * dpr;
n->devicePixelRatio = dpr;
// 1:1 mapping between the backing texture and the on-screen quad
m_widthMultiplier = 1.0f;
m_heightMultiplier = 1.0f;
} else {
QSize itemPixelSize = QSize(width(), height()) * dpr;
// not 1:1 maping between the backing texture and the on-screen quad
m_widthMultiplier = desiredFboSize.width() / float(itemPixelSize.width());
m_heightMultiplier = desiredFboSize.height() / float(itemPixelSize.height());
n->devicePixelRatio = 1.0;
}
desiredFboSize.setWidth(qMax(minFboSize.width(), desiredFboSize.width()));
desiredFboSize.setHeight(qMax(minFboSize.height(), desiredFboSize.height()));
if (desiredFboSize != m_effectiveTextureSize) {
m_effectiveTextureSize = desiredFboSize;
emit effectiveTextureSizeChanged();
}
n->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest);
n->setRect(0, 0, width(), height());
if (checkIsVisible() && isComponentComplete()) {
n->renderer->synchronize(this, desiredFboSize, n->devicePixelRatio);
if (n->renderer->m_textureNeedsFlip)
n->setTextureCoordinatesTransform(QSGSimpleTextureNode::MirrorVertically);
updateDynamicTextures();
n->scheduleRender();
}
return n;
}
QSGNode *QQuick3DViewport::setupInlineRenderer(QSGNode *node)
{
QQuick3DSGRenderNode *n = static_cast<QQuick3DSGRenderNode *>(node);
if (!n) {
if (!m_renderNode)
m_renderNode = new QQuick3DSGRenderNode;
n = m_renderNode;
}
if (!n->renderer) {
n->window = window();
n->renderer = createRenderer();
if (!n->renderer)
return nullptr;
}
if (!m_effectiveTextureSize.isEmpty()) {
m_effectiveTextureSize = QSize();
emit effectiveTextureSizeChanged();
}
const QSize targetSize = window()->effectiveDevicePixelRatio() * QSize(width(), height());
// checkIsVisible, not isVisible, because, for example, a
// { visible: false; layer.enabled: true } item still needs
// to function normally.
if (checkIsVisible() && isComponentComplete()) {
n->renderer->synchronize(this, targetSize, window()->effectiveDevicePixelRatio());
updateDynamicTextures();
n->markDirty(QSGNode::DirtyMaterial);
}
return n;
}
void QQuick3DViewport::setupDirectRenderer(RenderMode mode)
{
auto renderMode = (mode == Underlay) ? QQuick3DSGDirectRenderer::Underlay
: QQuick3DSGDirectRenderer::Overlay;
if (!m_directRenderer) {
QQuick3DSceneRenderer *sceneRenderer = createRenderer();
if (!sceneRenderer)
return;
m_directRenderer = new QQuick3DSGDirectRenderer(sceneRenderer, window(), renderMode);
connect(window(), &QQuickWindow::sceneGraphInvalidated, this, &QQuick3DViewport::cleanupDirectRenderer, Qt::DirectConnection);
}
if (!m_effectiveTextureSize.isEmpty()) {
m_effectiveTextureSize = QSize();
emit effectiveTextureSizeChanged();
}
const QSizeF targetSize = window()->effectiveDevicePixelRatio() * QSizeF(width(), height());
m_directRenderer->setViewport(QRectF(window()->effectiveDevicePixelRatio() * mapToScene(QPointF(0, 0)), targetSize));
m_directRenderer->setVisibility(isVisible());
if (isVisible()) {
m_directRenderer->preSynchronize();
m_directRenderer->renderer()->synchronize(this, targetSize.toSize(), window()->effectiveDevicePixelRatio());
updateDynamicTextures();
m_directRenderer->requestRender();
}
}
// This is used for offscreen mode since we need to check if
// this item is used by an effect but hidden
bool QQuick3DViewport::checkIsVisible() const
{
auto childPrivate = QQuickItemPrivate::get(this);
return (childPrivate->explicitVisible ||
(childPrivate->extra.isAllocated() && childPrivate->extra->effectRefCount));
}
/*!
\internal
This method processes a pickResult on an object with the objective of checking
if there are any QQuickItem based subscenes that the QPointerEvent needs to be
forwarded to (3D -> 2D). If such a subscene is found, the event will be mapped
to the correct cordinate system, and added to the visitedSubscenes map for later
event delivery.
*/
void QQuick3DViewport::processPickedObject(const QSSGRenderPickResult &pickResult,
int pointIndex,
QPointerEvent *event,
QFlatMap<QQuickItem *, SubsceneInfo> &visitedSubscenes) const
{
QQuickItem *subsceneRootItem = nullptr;
QPointF subscenePosition;
const auto backendObject = pickResult.m_hitObject;
const auto frontendObject = findFrontendNode(backendObject);
if (!frontendObject)
return;
// Figure out if there are any QQuickItem based scenes we need to forward
// the event to, and if the are found, determine how to translate the UV Coords
// in the pickResult based on what type the object containing the scene is.
auto frontendObjectPrivate = QQuick3DObjectPrivate::get(frontendObject);
if (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Item2D) {
// Item2D, this is the case where there is just an embedded Qt Quick 2D Item
// rendered directly to the scene.
auto item2D = qobject_cast<QQuick3DItem2D *>(frontendObject);
if (item2D)
subsceneRootItem = item2D->contentItem();
if (!subsceneRootItem || subsceneRootItem->childItems().isEmpty())
return; // ignore empty 2D subscenes
// In this case the "UV" coordinates are in pixels in the subscene root item's coordinate system.
subscenePosition = pickResult.m_localUVCoords.toPointF();
// The following code will account for custom input masking, as well any
// transformations that might have been applied to the Item
if (!subsceneRootItem->childAt(subscenePosition.x(), subscenePosition.y()))
return;
} else if (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Model) {
// Model
int materialSubset = pickResult.m_subset;
const auto backendModel = static_cast<const QSSGRenderModel *>(backendObject);
// Get material
if (backendModel->materials.size() < (pickResult.m_subset + 1))
materialSubset = backendModel->materials.size() - 1;
if (materialSubset < 0)
return;
const auto backendMaterial = backendModel->materials.at(materialSubset);
const auto frontendMaterial = static_cast<QQuick3DMaterial*>(findFrontendNode(backendMaterial));
subsceneRootItem = getSubSceneRootItem(frontendMaterial);
if (subsceneRootItem) {
// In this case the pick result really is using UV coordinates.
subscenePosition = QPointF(subsceneRootItem->x() + pickResult.m_localUVCoords.x() * subsceneRootItem->width(),
subsceneRootItem->y() - pickResult.m_localUVCoords.y() * subsceneRootItem->height() + subsceneRootItem->height());
}
}
// Add the new event (item and position) to the visitedSubscene map.
if (subsceneRootItem) {
SubsceneInfo &subscene = visitedSubscenes[subsceneRootItem]; // create if not found
subscene.obj = frontendObject;
if (subscene.eventPointScenePositions.size() != event->pointCount()) {
// ensure capacity, and use an out-of-scene position rather than 0,0 by default
constexpr QPointF inf(-qt_inf(), -qt_inf());
subscene.eventPointScenePositions.resize(event->pointCount(), inf);
}
subscene.eventPointScenePositions[pointIndex] = subscenePosition;
}
}
/*!
\internal
This method will try and find a QQuickItem based by looking for sourceItem()
properties on the primary color channels for the various material types.
\note for CustomMaterial there is just a best effort given where the first
associated Texture with a sourceItem property is used.
*/
QQuickItem *QQuick3DViewport::getSubSceneRootItem(QQuick3DMaterial *material) const
{
if (!material)
return nullptr;
QQuickItem *subsceneRootItem = nullptr;
const auto frontendMaterialPrivate = QQuick3DObjectPrivate::get(material);
if (frontendMaterialPrivate->type == QQuick3DObjectPrivate::Type::DefaultMaterial) {
// Default Material
const auto defaultMaterial = qobject_cast<QQuick3DDefaultMaterial *>(material);
if (defaultMaterial) {
// Just check for a diffuseMap for now
if (defaultMaterial->diffuseMap() && defaultMaterial->diffuseMap()->sourceItem())
subsceneRootItem = defaultMaterial->diffuseMap()->sourceItem();
}
} else if (frontendMaterialPrivate->type == QQuick3DObjectPrivate::Type::PrincipledMaterial) {
// Principled Material
const auto principledMaterial = qobject_cast<QQuick3DPrincipledMaterial *>(material);
if (principledMaterial) {
// Just check for a baseColorMap for now
if (principledMaterial->baseColorMap() && principledMaterial->baseColorMap()->sourceItem())
subsceneRootItem = principledMaterial->baseColorMap()->sourceItem();
}
} else if (frontendMaterialPrivate->type == QQuick3DObjectPrivate::Type::SpecularGlossyMaterial) {
// SpecularGlossy Material
const auto specularGlossyMaterial = qobject_cast<QQuick3DSpecularGlossyMaterial *>(material);
if (specularGlossyMaterial) {
// Just check for a albedoMap for now
if (specularGlossyMaterial->albedoMap() && specularGlossyMaterial->albedoMap()->sourceItem())
subsceneRootItem = specularGlossyMaterial->albedoMap()->sourceItem();
}
} else if (frontendMaterialPrivate->type == QQuick3DObjectPrivate::Type::CustomMaterial) {
// Custom Material
const auto customMaterial = qobject_cast<QQuick3DCustomMaterial *>(material);
if (customMaterial) {
// This case is a bit harder because we can not know how the textures will be used
const auto &texturesInputs = customMaterial->m_dynamicTextureMaps;
for (const auto &textureInput : texturesInputs) {
if (auto texture = textureInput->texture()) {
if (texture->sourceItem()) {
subsceneRootItem = texture->sourceItem();
break;
}
}
}
}
}
return subsceneRootItem;
}
/*!
\internal
*/
QQuick3DPickResult QQuick3DViewport::getNearestPickResult(const QVarLengthArray<QSSGRenderPickResult, 20> &pickResults) const
{
for (const auto &result : pickResults) {
auto pickResult = processPickResult(result);
if (pickResult.hitType() != QQuick3DPickResultEnums::HitType::Null)
return pickResult;
}
return QQuick3DPickResult();
}
/*!
\internal
This method is responsible for going through the visitedSubscenes map and forwarding
the event with corrected coordinates to each subscene.
*/
bool QQuick3DViewport::forwardEventToSubscenes(QPointerEvent *event,
bool useRayPicking,
QQuick3DSceneRenderer *renderer,
const QFlatMap<QQuickItem *, SubsceneInfo> &visitedSubscenes) const
{
// Now deliver the entire event (all points) to each relevant subscene.
// Maybe only some points fall inside, but QQuickDeliveryAgentPrivate::deliverMatchingPointsToItem()
// makes reduced-subset touch events that contain only the relevant points, when necessary.
bool ret = false;
QVarLengthArray<QPointF, 16> originalScenePositions;
originalScenePositions.resize(event->pointCount());
for (int pointIndex = 0; pointIndex < event->pointCount(); ++pointIndex)
originalScenePositions[pointIndex] = event->point(pointIndex).scenePosition();
for (const auto &subscene : visitedSubscenes) {
QQuickItem *subsceneRoot = subscene.first;
const auto &subsceneInfo = subscene.second;
Q_ASSERT(subsceneInfo.eventPointScenePositions.size() == event->pointCount());
auto da = QQuickItemPrivate::get(subsceneRoot)->deliveryAgent();
for (int pointIndex = 0; pointIndex < event->pointCount(); ++pointIndex) {
const auto &pt = subsceneInfo.eventPointScenePositions.at(pointIndex);
// By tradition, QGuiApplicationPrivate::processTouchEvent() has set the local position to the scene position,
// and Qt Quick expects it to arrive that way: then QQuickDeliveryAgentPrivate::translateTouchEvent()
// copies it into the scene position before localizing.
// That might be silly, we might change it eventually, but gotta stay consistent for now.
QEventPoint &ep = event->point(pointIndex);
QMutableEventPoint::setPosition(ep, pt);
QMutableEventPoint::setScenePosition(ep, pt);
}
if (event->isBeginEvent())
da->setSceneTransform(nullptr);
if (da->event(event)) {
ret = true;
if (QQuickDeliveryAgentPrivate::anyPointGrabbed(event) && !useRayPicking) {
// In case any QEventPoint was grabbed, the relevant QQuickDeliveryAgent needs to know
// how to repeat the picking/coordinate transformation for each update,
// because delivery will bypass internalPick() due to the grab, and it's
// more efficient to avoid whole-scene picking each time anyway.
auto frontendObjectPrivate = QQuick3DObjectPrivate::get(subsceneInfo.obj);
const bool item2Dcase = (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Item2D);
ViewportTransformHelper *transform = new ViewportTransformHelper;
transform->viewport = const_cast<QQuick3DViewport *>(this);
transform->renderer = renderer;
transform->sceneParentNode = static_cast<QSSGRenderNode*>(frontendObjectPrivate->spatialNode);
transform->targetItem = subsceneRoot;
transform->scaleX = window()->effectiveDevicePixelRatio() * m_widthMultiplier;
transform->scaleY = window()->effectiveDevicePixelRatio() * m_heightMultiplier;
transform->uvCoordsArePixels = item2Dcase;
transform->setOnDeliveryAgent(da);
qCDebug(lcPick) << event->type() << "created ViewportTransformHelper on" << da;
}
} else if (event->type() != QEvent::HoverMove) {
qCDebug(lcPick) << subsceneRoot << "didn't want" << event;
}
event->setAccepted(false); // reject implicit grab and let it keep propagating
}
if (visitedSubscenes.isEmpty()) {
event->setAccepted(false);
} else {
for (int pointIndex = 0; pointIndex < event->pointCount(); ++pointIndex)
QMutableEventPoint::setScenePosition(event->point(pointIndex), originalScenePositions.at(pointIndex));
}
// Normally this would occur in QQuickWindowPrivate::clearGrabbers(...) but
// for ray based input, input never goes through QQuickWindow (since events
// are generated from within scene space and not window/screen space).
if (event->isEndEvent() && useRayPicking) {
if (event->isSinglePointEvent()) {
if (static_cast<QSinglePointEvent *>(event)->buttons() == Qt::NoButton) {
auto &firstPt = event->point(0);
event->setExclusiveGrabber(firstPt, nullptr);
event->clearPassiveGrabbers(firstPt);
}
} else {
for (auto &point : event->points()) {
if (point.state() == QEventPoint::State::Released) {
event->setExclusiveGrabber(point, nullptr);
event->clearPassiveGrabbers(point);
}
}
}
}
return ret;
}
bool QQuick3DViewport::internalPick(QPointerEvent *event, const QVector3D &origin, const QVector3D &direction) const
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer || !event)
return false;
QFlatMap<QQuickItem*, SubsceneInfo> visitedSubscenes;
const bool useRayPicking = !direction.isNull();
for (int pointIndex = 0; pointIndex < event->pointCount(); ++pointIndex) {
auto &eventPoint = event->point(pointIndex);
QVarLengthArray<QSSGRenderPickResult, 20> pickResults;
if (Q_UNLIKELY(useRayPicking))
pickResults = getPickResults(renderer, origin, direction);
else
pickResults = getPickResults(renderer, eventPoint);
if (!pickResults.isEmpty())
for (const auto &pickResult : pickResults)
processPickedObject(pickResult, pointIndex, event, visitedSubscenes);
else
eventPoint.setAccepted(false); // let it fall through the viewport to Items underneath
}
return forwardEventToSubscenes(event, useRayPicking, renderer, visitedSubscenes);
}
bool QQuick3DViewport::singlePointPick(QSinglePointEvent *event, const QVector3D &origin, const QVector3D &direction)
{
QQuick3DSceneRenderer *renderer = getRenderer();
if (!renderer || !event)
return false;
QSSGRenderRay ray(origin, direction);
Q_ASSERT(event->pointCount() == 1);
QPointF originalPosition = event->point(0).scenePosition();
auto pickResults = renderer->syncPickAll(ray);
bool delivered = false;
constexpr float jitterLimit = 2.5; // TODO: add property for this?
bool withinJitterLimit = false;
for (const auto &pickResult : pickResults) {
auto [item, position] = getItemAndPosition(pickResult);
if (!item)
break;
if (item == m_prevMouseItem && (position - m_prevMousePos).manhattanLength() < jitterLimit && !event->button()) {
withinJitterLimit = true;
break;
}
auto da = QQuickItemPrivate::get(item)->deliveryAgent();
QEventPoint &ep = event->point(0);
QMutableEventPoint::setPosition(ep, position);
QMutableEventPoint::setScenePosition(ep, position);
if (da->event(event)) {
delivered = true;
if (event->type() == QEvent::MouseButtonPress) {
m_prevMouseItem = item;
m_prevMousePos = position;
withinJitterLimit = true;
}
break;
}
}
QMutableEventPoint::setScenePosition(event->point(0), originalPosition);
if (!withinJitterLimit)
m_prevMouseItem = nullptr;
// Normally this would occur in QQuickWindowPrivate::clearGrabbers(...) but
// for ray based input, input never goes through QQuickWindow (since events
// are generated from within scene space and not window/screen space).
if (event->isEndEvent()) {
if (event->buttons() == Qt::NoButton) {
auto &firstPt = event->point(0);
event->setExclusiveGrabber(firstPt, nullptr);
event->clearPassiveGrabbers(firstPt);
}
}
return delivered;
}
QPair<QQuickItem *, QPointF> QQuick3DViewport::getItemAndPosition(const QSSGRenderPickResult &pickResult) const
{
QQuickItem *subsceneRootItem = nullptr;
QPointF subscenePosition;
const auto backendObject = pickResult.m_hitObject;
const auto frontendObject = findFrontendNode(backendObject);
if (!frontendObject)
return {};
auto frontendObjectPrivate = QQuick3DObjectPrivate::get(frontendObject);
if (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Item2D) {
// Item2D, this is the case where there is just an embedded Qt Quick 2D Item
// rendered directly to the scene.
auto item2D = qobject_cast<QQuick3DItem2D *>(frontendObject);
if (item2D)
subsceneRootItem = item2D->contentItem();
if (!subsceneRootItem || subsceneRootItem->childItems().isEmpty())
return {}; // ignore empty 2D subscenes
// In this case the "UV" coordinates are in pixels in the subscene root item's coordinate system.
subscenePosition = pickResult.m_localUVCoords.toPointF();
// The following code will account for custom input masking, as well any
// transformations that might have been applied to the Item
if (!subsceneRootItem->childAt(subscenePosition.x(), subscenePosition.y()))
return {};
} else if (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Model) {
// Model
int materialSubset = pickResult.m_subset;
const auto backendModel = static_cast<const QSSGRenderModel *>(backendObject);
// Get material
if (backendModel->materials.size() < (pickResult.m_subset + 1))
materialSubset = backendModel->materials.size() - 1;
if (materialSubset < 0)
return {};
const auto backendMaterial = backendModel->materials.at(materialSubset);
const auto frontendMaterial = static_cast<QQuick3DMaterial *>(findFrontendNode(backendMaterial));
subsceneRootItem = getSubSceneRootItem(frontendMaterial);
if (subsceneRootItem) {
// In this case the pick result really is using UV coordinates.
subscenePosition = QPointF(subsceneRootItem->x() + pickResult.m_localUVCoords.x() * subsceneRootItem->width(),
subsceneRootItem->y() - pickResult.m_localUVCoords.y() * subsceneRootItem->height() + subsceneRootItem->height());
}
}
return {subsceneRootItem, subscenePosition};
}
QVarLengthArray<QSSGRenderPickResult, 20> QQuick3DViewport::getPickResults(QQuick3DSceneRenderer *renderer,
const QVector3D &origin,
const QVector3D &direction) const
{
const QSSGRenderRay ray(origin, direction);
return renderer->syncPickAll(ray);
}
QVarLengthArray<QSSGRenderPickResult, 20> QQuick3DViewport::getPickResults(QQuick3DSceneRenderer *renderer, const QEventPoint &eventPoint) const
{
QVarLengthArray<QSSGRenderPickResult, 20> pickResults;
QPointF realPosition = eventPoint.position() * window()->effectiveDevicePixelRatio();
// correct when mapping is not 1:1 due to explicit backing texture size
realPosition.rx() *= m_widthMultiplier;
realPosition.ry() *= m_heightMultiplier;
std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(realPosition);
if (rayResult.has_value())
pickResults = renderer->syncPickAll(rayResult.value());
return pickResults;
}
/*!
\internal
This provides a way to lookup frontendNodes with a backend node taking into consideration both
the scene and the importScene
*/
QQuick3DObject *QQuick3DViewport::findFrontendNode(const QSSGRenderGraphObject *backendObject) const
{
if (!backendObject)
return nullptr;
const auto sceneManager = QQuick3DObjectPrivate::get(m_sceneRoot)->sceneManager;
QQuick3DObject *frontendObject = sceneManager->lookUpNode(backendObject);
if (!frontendObject && m_importScene) {
const auto importSceneManager = QQuick3DObjectPrivate::get(m_importScene)->sceneManager;
frontendObject = importSceneManager->lookUpNode(backendObject);
}
return frontendObject;
}
QQuick3DPickResult QQuick3DViewport::processPickResult(const QSSGRenderPickResult &pickResult) const
{
if (!pickResult.m_hitObject)
return QQuick3DPickResult();
QQuick3DObject *frontendObject = findFrontendNode(pickResult.m_hitObject);
QQuick3DModel *model = qobject_cast<QQuick3DModel *>(frontendObject);
if (model) {
auto itemAndPosition = getItemAndPosition(pickResult);
return QQuick3DPickResult(model,
::sqrtf(pickResult.m_distanceSq),
pickResult.m_localUVCoords,
pickResult.m_scenePosition,
pickResult.m_localPosition,
pickResult.m_faceNormal,
pickResult.m_sceneNormal,
pickResult.m_instanceIndex,
itemAndPosition.first);
}
QQuick3DItem2D *frontend2DItem = qobject_cast<QQuick3DItem2D *>(frontendObject);
if (frontend2DItem && frontend2DItem->contentItem()) {
// Check if the pick is inside the content item (since the ray just intersected on the items plane)
const QPointF subscenePosition = pickResult.m_localUVCoords.toPointF();
const auto child = frontend2DItem->contentItem()->childAt(subscenePosition.x(), subscenePosition.y());
if (child) {
return QQuick3DPickResult(child,
::sqrtf(pickResult.m_distanceSq),
QVector2D(frontend2DItem->contentItem()->mapToItem(child, subscenePosition)),
pickResult.m_scenePosition,
pickResult.m_localPosition,
pickResult.m_faceNormal);
}
}
return QQuick3DPickResult();
}
// Returns the first found scene manager of objects children
QQuick3DSceneManager *QQuick3DViewport::findChildSceneManager(QQuick3DObject *inObject, QQuick3DSceneManager *manager)
{
if (manager)
return manager;
auto children = QQuick3DObjectPrivate::get(inObject)->childItems;
for (auto *child : std::as_const(children)) {
if (auto m = QQuick3DObjectPrivate::get(child)->sceneManager) {
manager = m;
break;
}
manager = findChildSceneManager(child, manager);
}
return manager;
}
void QQuick3DViewport::updateInputProcessing()
{
// This should be called from the gui thread.
setAcceptTouchEvents(m_enableInputProcessing);
setAcceptHoverEvents(m_enableInputProcessing);
setAcceptedMouseButtons(m_enableInputProcessing ? Qt::AllButtons : Qt::NoButton);
}
void QQuick3DViewport::onReleaseCachedResources()
{
if (auto renderer = getRenderer())
renderer->releaseCachedResources();
}
/*!
\qmlproperty List<QtQuick3D::Object3D> View3D::extensions
This property contains a list of user extensions that should be used with this \l View3D.
\sa RenderExtension
*/
QQmlListProperty<QQuick3DObject> QQuick3DViewport::extensions()
{
return QQmlListProperty<QQuick3DObject>{ this,
&m_extensionListDirty,
&QQuick3DExtensionListHelper::extensionAppend,
&QQuick3DExtensionListHelper::extensionCount,
&QQuick3DExtensionListHelper::extensionAt,
&QQuick3DExtensionListHelper::extensionClear,
&QQuick3DExtensionListHelper::extensionReplace,
&QQuick3DExtensionListHelper::extensionRemoveLast};
}
/*!
\internal
*/
void QQuick3DViewport::rebuildExtensionList()
{
m_extensionListDirty = true;
update();
}
/*!
\internal
Private constructor for the QQuick3DViewport class so we can differentiate between
a regular QQuick3DViewport and one created for a specific usage, like XR.
*/
QQuick3DViewport::QQuick3DViewport(PrivateInstanceType type, QQuickItem *parent)
: QQuick3DViewport(parent)
{
m_isXrViewInstance = type == PrivateInstanceType::XrViewInstance;
}
void QQuick3DViewport::updateCameraForLayer(const QQuick3DViewport &view3D, QSSGRenderLayer &layerNode)
{
layerNode.explicitCameras.clear();
if (!view3D.m_multiViewCameras.isEmpty()) {
for (QQuick3DCamera *camera : std::as_const(view3D.m_multiViewCameras))
layerNode.explicitCameras.append(static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(camera)->spatialNode));
} else if (view3D.camera()) {
if (QSSGRenderCamera *camera = static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(view3D.camera())->spatialNode))
layerNode.explicitCameras.append(camera);
}
// Ensure these have a parent. All nodes need to be in the node tree somewhere, even if they're technically "parentless"
// or we'll not assign a storage slot for them or update them.
for (QSSGRenderCamera *camera : std::as_const(layerNode.explicitCameras)) {
if (!camera->parent)
layerNode.addChild(*camera);
}
}
void QQuick3DViewport::updateSceneManagerForImportScene()
{
auto privateObject = QQuick3DObjectPrivate::get(m_importScene);
if (!privateObject->sceneManager) {
// If object doesn't already have scene manager, check from its children
QQuick3DSceneManager *manager = findChildSceneManager(m_importScene);
// If still not found, use the one from the scene root (scenes defined outside of an view3d)
if (!manager)
manager = QQuick3DObjectPrivate::get(m_sceneRoot)->sceneManager;
if (manager) {
manager->setWindow(window());
privateObject->refSceneManager(*manager);
}
// In the case m_sceneRoot doesn't have a valid sceneManager,
// it means the view3d is not valid now.
if (!privateObject->sceneManager)
return;
}
connect(privateObject->sceneManager, &QQuick3DSceneManager::needsUpdate,
this, &QQuickItem::update, Qt::UniqueConnection);
connect(privateObject->sceneManager, &QObject::destroyed,
this, [&](QObject *) {
auto privateObject = QQuick3DObjectPrivate::get(m_importScene);
privateObject->sceneManager = nullptr;
updateSceneManagerForImportScene();
}, Qt::DirectConnection);
QQuick3DNode *scene = m_importScene;
while (scene) {
QQuick3DSceneRootNode *rn = qobject_cast<QQuick3DSceneRootNode *>(scene);
scene = rn ? rn->view3D()->importScene() : nullptr;
if (scene) {
connect(QQuick3DObjectPrivate::get(scene)->sceneManager,
&QQuick3DSceneManager::needsUpdate,
this, &QQuickItem::update, Qt::UniqueConnection);
}
}
}
/*!
\qmlproperty enumeration QtQuick3D::View3D::renderOverrides
Controls how Qt Quick 3D performs rendering.
\value View3D.None Rendering proceeds normally. Internal passes are scheduled as required by the
features used in the application.
\value View3D.DisableInternalPasses Disables Qt Quick 3D's internal rendering passes.
When this mode is set, the application is responsible for producing and presenting the final frame.
Typically, this involves implementing custom \l {User-Defined Render Passes in Qt Quick 3D}
{user passes} and presenting the result to the viewport using \l {SimpleQuadRenderer} or a
custom \l {QQuick3DRenderExtension}{render extension}.
*/
QQuick3DViewport::RenderOverrides QQuick3DViewport::renderOverrides() const
{
return m_renderOverrides;
}
void QQuick3DViewport::setRenderOverrides(RenderOverrides newRenderOverrides)
{
if (m_renderOverrides == newRenderOverrides)
return;
m_renderOverrides = newRenderOverrides;
emit renderOverridesChanged();
update();
}
QT_END_NAMESPACE