1 | // Copyright (C) 2019 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qquick3dviewport_p.h" |
5 | #include "qquick3dsceneenvironment_p.h" |
6 | #include "qquick3dscenemanager_p.h" |
7 | #include "qquick3dtexture_p.h" |
8 | #include "qquick3dscenerenderer_p.h" |
9 | #include "qquick3dscenerootnode_p.h" |
10 | #include "qquick3dcamera_p.h" |
11 | #include "qquick3dmodel_p.h" |
12 | #include "qquick3drenderstats_p.h" |
13 | #include "qquick3ditem2d_p.h" |
14 | #include "qquick3ddefaultmaterial_p.h" |
15 | #include "qquick3dprincipledmaterial_p.h" |
16 | #include "qquick3dcustommaterial_p.h" |
17 | #include "qquick3dspecularglossymaterial_p.h" |
18 | |
19 | #include <QtQuick3DRuntimeRender/private/qssgrenderlayer_p.h> |
20 | #include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h> |
21 | |
22 | #include <QtQuick3DUtils/private/qssgassert_p.h> |
23 | |
24 | #include <qsgtextureprovider.h> |
25 | #include <QSGSimpleTextureNode> |
26 | #include <QSGRendererInterface> |
27 | #include <QQuickWindow> |
28 | #include <QtQuick/private/qquickitem_p.h> |
29 | #include <QtQuick/private/qquickpointerhandler_p.h> |
30 | |
31 | #include <QtQml> |
32 | |
33 | #include <QtGui/private/qeventpoint_p.h> |
34 | |
35 | #include <QtCore/private/qnumeric_p.h> |
36 | #include <QtCore/qpointer.h> |
37 | |
38 | #include <optional> |
39 | |
40 | QT_BEGIN_NAMESPACE |
41 | |
42 | Q_LOGGING_CATEGORY(lcEv, "qt.quick3d.event" ) |
43 | Q_LOGGING_CATEGORY(lcPick, "qt.quick3d.pick" ) |
44 | Q_LOGGING_CATEGORY(lcHover, "qt.quick3d.hover" ) |
45 | |
46 | static bool isforceInputHandlingSet() |
47 | { |
48 | static const bool v = (qEnvironmentVariableIntValue(varName: "QT_QUICK3D_FORCE_INPUT_HANDLING" ) > 0); |
49 | return v; |
50 | } |
51 | |
52 | struct ViewportTransformHelper : public QQuickDeliveryAgent::Transform |
53 | { |
54 | static void removeAll() { |
55 | for (auto o : owners) { |
56 | if (!o.isNull()) |
57 | o->setSceneTransform(nullptr); |
58 | } |
59 | owners.clear(); |
60 | } |
61 | |
62 | void setOnDeliveryAgent(QQuickDeliveryAgent *da) { |
63 | da->setSceneTransform(this); |
64 | owners.append(t: da); |
65 | } |
66 | |
67 | /* |
68 | Transforms viewport coordinates to 2D scene coordinates. |
69 | Returns the point in targetItem corresponding to \a viewportPoint, |
70 | assuming that targetItem is mapped onto sceneParentNode. |
71 | If it's no longer a "hit" on sceneParentNode, returns the last-good point. |
72 | */ |
73 | QPointF map(const QPointF &viewportPoint) override { |
74 | QPointF point = viewportPoint; |
75 | // Despite the name, the input coordinates are the window viewport coordinates |
76 | // so unless the View3D is the same size of the Window, we need to translate |
77 | // to the View3D coordinates before doing any picking. |
78 | if (viewport) |
79 | point = viewport->mapFromScene(point: viewportPoint); |
80 | point.rx() *= scaleX; |
81 | point.ry() *= scaleY; |
82 | std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(pos: point); |
83 | if (rayResult.has_value()) { |
84 | const auto pickResults = renderer->syncPickOne(ray: rayResult.value(), node: sceneParentNode); |
85 | if (!pickResults.isEmpty()) { |
86 | const auto pickResult = pickResults.first(); |
87 | auto ret = pickResult.m_localUVCoords.toPointF(); |
88 | if (!uvCoordsArePixels) { |
89 | ret = QPointF(targetItem->x() + ret.x() * targetItem->width(), |
90 | targetItem->y() - ret.y() * targetItem->height() + targetItem->height()); |
91 | } |
92 | const bool outOfModel = pickResult.m_localUVCoords.isNull(); |
93 | qCDebug(lcEv) << viewportPoint << "->" << (outOfModel ? "OOM" : "" ) << ret << "@" << pickResult.m_scenePosition |
94 | << "UV" << pickResult.m_localUVCoords << "dist" << qSqrt(v: pickResult.m_distanceSq); |
95 | if (outOfModel) { |
96 | return lastGoodMapping; |
97 | } else { |
98 | lastGoodMapping = ret; |
99 | return ret; |
100 | } |
101 | } |
102 | } |
103 | return QPointF(); |
104 | } |
105 | |
106 | QPointer<QQuick3DViewport> viewport; |
107 | QQuick3DSceneRenderer *renderer = nullptr; |
108 | QSSGRenderNode *sceneParentNode = nullptr; |
109 | QPointer<QQuickItem> targetItem; |
110 | qreal scaleX = 1; |
111 | qreal scaleY = 1; |
112 | bool uvCoordsArePixels = false; // if false, they are in the range 0..1 |
113 | QPointF lastGoodMapping; |
114 | |
115 | static QList<QPointer<QQuickDeliveryAgent>> owners; |
116 | }; |
117 | |
118 | QList<QPointer<QQuickDeliveryAgent>> ViewportTransformHelper::owners; |
119 | |
120 | class QQuick3DExtensionListHelper |
121 | { |
122 | Q_DISABLE_COPY_MOVE(QQuick3DExtensionListHelper); |
123 | public: |
124 | static void extensionAppend(QQmlListProperty<QQuick3DObject> *list, QQuick3DObject *extension) |
125 | { |
126 | QSSG_ASSERT(list && extension, return); |
127 | |
128 | if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(object: list->object)) { |
129 | if (const auto idx = that->m_extensions.indexOf(t: extension); idx == -1) { |
130 | if (!extension->parentItem()) |
131 | extension->setParentItem(that->m_sceneRoot); |
132 | that->m_extensions.push_back(t: extension); |
133 | that->m_extensionListDirty = true; |
134 | } |
135 | } |
136 | } |
137 | static QQuick3DObject *extensionAt(QQmlListProperty<QQuick3DObject> *list, qsizetype index) |
138 | { |
139 | QQuick3DObject *ret = nullptr; |
140 | QSSG_ASSERT(list, return ret); |
141 | |
142 | if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(object: list->object)) { |
143 | if (that->m_extensions.size() > index) |
144 | ret = that->m_extensions.at(i: index); |
145 | } |
146 | |
147 | return ret; |
148 | } |
149 | static qsizetype extensionCount(QQmlListProperty<QQuick3DObject> *list) |
150 | { |
151 | qsizetype ret = -1; |
152 | QSSG_ASSERT(list, return ret); |
153 | |
154 | if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(object: list->object)) |
155 | ret = that->m_extensions.size(); |
156 | |
157 | return ret; |
158 | } |
159 | static void extensionClear(QQmlListProperty<QQuick3DObject> *list) |
160 | { |
161 | QSSG_ASSERT(list, return); |
162 | |
163 | if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(object: list->object)) { |
164 | that->m_extensions.clear(); |
165 | that->m_extensionListDirty = true; |
166 | } |
167 | } |
168 | static void extensionReplace(QQmlListProperty<QQuick3DObject> *list, qsizetype idx, QQuick3DObject *o) |
169 | { |
170 | QSSG_ASSERT(list, return); |
171 | |
172 | if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(object: list->object)) { |
173 | if (that->m_extensions.size() > idx && idx > -1) { |
174 | that->m_extensions.replace(i: idx, t: o); |
175 | that->m_extensionListDirty = true; |
176 | } |
177 | } |
178 | } |
179 | static void extensionRemoveLast(QQmlListProperty<QQuick3DObject> *list) |
180 | { |
181 | QSSG_ASSERT(list, return); |
182 | |
183 | if (QQuick3DViewport *that = qobject_cast<QQuick3DViewport *>(object: list->object)) { |
184 | that->m_extensions.removeLast(); |
185 | that->m_extensionListDirty = true; |
186 | } |
187 | } |
188 | }; |
189 | |
190 | /*! |
191 | \qmltype View3D |
192 | \inherits QQuickItem |
193 | \inqmlmodule QtQuick3D |
194 | \brief Provides a viewport on which to render a 3D scene. |
195 | |
196 | View3D provides a 2D surface on which a 3D scene can be rendered. This |
197 | surface is a Qt Quick \l Item and can be placed in a Qt Quick scene. |
198 | |
199 | There are two ways to define the 3D scene that is visualized on the View3D: |
200 | If you define a hierarchy of \l{Node}{Node-based} items as children of |
201 | the View3D directly, then this will become the implicit scene of the View3D. |
202 | |
203 | It is also possible to reference an existing scene by using the \l importScene |
204 | property and setting it to the root \l Node of the scene you want to visualize. |
205 | This \l Node does not have to be an ancestor of the View3D, and you can have |
206 | multiple View3Ds that import the same scene. |
207 | |
208 | This is demonstrated in \l {Qt Quick 3D - View3D example}{View3D example}. |
209 | |
210 | If the View3D both has child \l{Node}{Nodes} and the \l importScene property is |
211 | set simultaneously, then both scenes will be rendered as if they were sibling |
212 | subtrees in the same scene. |
213 | |
214 | To control how a scene is rendered, you can set the \l environment |
215 | property. The type \l SceneEnvironment has a number of visual properties |
216 | that can be adjusted, such as background color, tone mapping, anti-aliasing |
217 | and more. \l ExtendedSceneEnvironment in the \c{QtQuick3D.Helpers} module |
218 | extends \l SceneEnvironment with even more features, adding common |
219 | post-processing effects. |
220 | |
221 | In addition, in order for anything to be rendered in the View3D, the scene |
222 | needs a \l Camera. If there is only a single \l Camera in the scene, then |
223 | this will automatically be picked. Otherwise, the \l camera property can |
224 | be used to select the camera. The \l Camera decides which parts of the scene |
225 | are visible, and how they are projected onto the 2D surface. |
226 | |
227 | By default, the 3D scene will first be rendered into an off-screen buffer and |
228 | then composited with the rest of the Qt Quick scene when it is done. This provides |
229 | the maximum level of compatibility, but may have performance implications on some |
230 | graphics hardware. If this is the case, the \l renderMode property can be used to |
231 | switch how the View3D is rendered into the window. |
232 | |
233 | A View3D with the default Offscreen \l renderMode is implicitly a |
234 | \l{QSGTextureProvider}{texture provider} as well. This means that \l |
235 | ShaderEffect or \l{QtQuick3D::Texture::sourceItem}{Texture.sourceItem} can reference |
236 | the View3D directly as long as all items live within the same |
237 | \l{QQuickWindow}{window}. Like with any other \l Item, it is also possible |
238 | to switch the View3D, or one of its ancestors, into a texture-based |
239 | \l{QtQuick::Item::layer.enabled}{item layer}. |
240 | |
241 | \sa {Qt Quick 3D - View3D example} |
242 | */ |
243 | |
244 | QQuick3DViewport::QQuick3DViewport(QQuickItem *parent) |
245 | : QQuickItem(parent) |
246 | { |
247 | setFlag(flag: ItemHasContents); |
248 | m_camera = nullptr; |
249 | m_sceneRoot = new QQuick3DSceneRootNode(this); |
250 | m_environment = new QQuick3DSceneEnvironment(m_sceneRoot); |
251 | m_renderStats = new QQuick3DRenderStats(); |
252 | QQuick3DSceneManager *sceneManager = new QQuick3DSceneManager(); |
253 | QQuick3DObjectPrivate::get(item: m_sceneRoot)->refSceneManager(*sceneManager); |
254 | Q_ASSERT(sceneManager == QQuick3DObjectPrivate::get(m_sceneRoot)->sceneManager); |
255 | connect(sender: sceneManager, signal: &QQuick3DSceneManager::needsUpdate, |
256 | context: this, slot: &QQuickItem::update); |
257 | |
258 | // Overrides the internal input handling to always be true |
259 | // instead of potentially updated after a sync (see updatePaintNode) |
260 | if (isforceInputHandlingSet()) { |
261 | m_enableInputProcessing = true; |
262 | updateInputProcessing(); |
263 | forceActiveFocus(); |
264 | } |
265 | } |
266 | |
267 | QQuick3DViewport::~QQuick3DViewport() |
268 | { |
269 | // With the threaded render loop m_directRenderer must be destroyed on the |
270 | // render thread at the proper time, not here. That's handled in |
271 | // releaseResources() + upon sceneGraphInvalidated. However with a |
272 | // QQuickRenderControl-based window on the main thread there is a good |
273 | // chance that this viewport (and so our sceneGraphInvalidated signal |
274 | // connection) is destroyed before the window and the rendercontrol. So act |
275 | // here then. |
276 | if (m_directRenderer && m_directRenderer->thread() == thread()) { |
277 | delete m_directRenderer; |
278 | m_directRenderer = nullptr; |
279 | } |
280 | |
281 | // If the quick window still exists, make sure to disconnect any of the direct |
282 | // connections to this View3D |
283 | if (auto qw = window()) |
284 | disconnect(sender: qw, signal: nullptr, receiver: this, member: nullptr); |
285 | |
286 | auto sceneManager = QQuick3DObjectPrivate::get(item: m_sceneRoot)->sceneManager; |
287 | if (sceneManager) { |
288 | sceneManager->setParent(nullptr); |
289 | if (auto wa = sceneManager->wattached) |
290 | wa->queueForCleanup(manager: sceneManager); |
291 | } |
292 | |
293 | delete m_sceneRoot; |
294 | m_sceneRoot = nullptr; |
295 | |
296 | // m_renderStats is tightly coupled with the render thread, so can't delete while we |
297 | // might still be rendering. |
298 | m_renderStats->deleteLater(); |
299 | |
300 | if (!window() && sceneManager && sceneManager->wattached) |
301 | QMetaObject::invokeMethod(object: sceneManager->wattached, function: &QQuick3DWindowAttachment::evaluateEol, type: Qt::QueuedConnection); |
302 | } |
303 | |
304 | static void ssgn_append(QQmlListProperty<QObject> *property, QObject *obj) |
305 | { |
306 | if (!obj) |
307 | return; |
308 | QQuick3DViewport *view3d = static_cast<QQuick3DViewport *>(property->object); |
309 | |
310 | if (QQuick3DObject *sceneObject = qmlobject_cast<QQuick3DObject *>(object: obj)) { |
311 | QQmlListProperty<QObject> itemProperty = QQuick3DObjectPrivate::get(item: view3d->scene())->data(); |
312 | itemProperty.append(&itemProperty, sceneObject); |
313 | } else { |
314 | QQuickItemPrivate::data_append(property, obj); |
315 | } |
316 | } |
317 | |
318 | static qsizetype ssgn_count(QQmlListProperty<QObject> *property) |
319 | { |
320 | QQuick3DViewport *view3d = static_cast<QQuick3DViewport *>(property->object); |
321 | if (!view3d || !view3d->scene() || !QQuick3DObjectPrivate::get(item: view3d->scene())->data().count) |
322 | return 0; |
323 | QQmlListProperty<QObject> itemProperty = QQuick3DObjectPrivate::get(item: view3d->scene())->data(); |
324 | return itemProperty.count(&itemProperty); |
325 | } |
326 | |
327 | static QObject *ssgn_at(QQmlListProperty<QObject> *property, qsizetype i) |
328 | { |
329 | QQuick3DViewport *view3d = static_cast<QQuick3DViewport *>(property->object); |
330 | QQmlListProperty<QObject> itemProperty = QQuick3DObjectPrivate::get(item: view3d->scene())->data(); |
331 | return itemProperty.at(&itemProperty, i); |
332 | } |
333 | |
334 | static void ssgn_clear(QQmlListProperty<QObject> *property) |
335 | { |
336 | QQuick3DViewport *view3d = static_cast<QQuick3DViewport *>(property->object); |
337 | QQmlListProperty<QObject> itemProperty = QQuick3DObjectPrivate::get(item: view3d->scene())->data(); |
338 | return itemProperty.clear(&itemProperty); |
339 | } |
340 | |
341 | |
342 | QQmlListProperty<QObject> QQuick3DViewport::data() |
343 | { |
344 | return QQmlListProperty<QObject>(this, |
345 | nullptr, |
346 | ssgn_append, |
347 | ssgn_count, |
348 | ssgn_at, |
349 | ssgn_clear); |
350 | } |
351 | |
352 | /*! |
353 | \qmlproperty QtQuick3D::Camera QtQuick3D::View3D::camera |
354 | |
355 | This property specifies which \l Camera is used to render the scene. If this |
356 | property is not set, then the first enabled camera in the scene will be used. |
357 | |
358 | \note If this property contains a camera that's not \l {Node::visible}{visible} then |
359 | no further attempts to find a camera will be done. |
360 | |
361 | \sa PerspectiveCamera, OrthographicCamera, FrustumCamera, CustomCamera |
362 | */ |
363 | QQuick3DCamera *QQuick3DViewport::camera() const |
364 | { |
365 | return m_camera; |
366 | } |
367 | |
368 | /*! |
369 | \qmlproperty QtQuick3D::SceneEnvironment QtQuick3D::View3D::environment |
370 | |
371 | This property specifies the SceneEnvironment used to render the scene. |
372 | |
373 | \sa SceneEnvironment |
374 | */ |
375 | QQuick3DSceneEnvironment *QQuick3DViewport::environment() const |
376 | { |
377 | return m_environment; |
378 | } |
379 | |
380 | /*! |
381 | \qmlproperty QtQuick3D::Node QtQuick3D::View3D::scene |
382 | \readonly |
383 | |
384 | Returns the root \l Node of the View3D's scene. |
385 | |
386 | To define the 3D scene that is visualized in the View3D: |
387 | |
388 | \list |
389 | \li Define a hierarchy of \l{Node}{Node-based} items as children of |
390 | the View3D directly, then this will become the implicit \l scene of the |
391 | View3D. |
392 | \li Reference an existing scene by using the \l importScene property and |
393 | set it to the root \l Node of the scene you want to visualize. This |
394 | \l Node does not have to be an ancestor of the View3D, and you can have |
395 | multiple View3Ds that import the same scene. |
396 | \endlist |
397 | |
398 | \sa importScene |
399 | */ |
400 | QQuick3DNode *QQuick3DViewport::scene() const |
401 | { |
402 | return m_sceneRoot; |
403 | } |
404 | |
405 | /*! |
406 | \qmlproperty QtQuick3D::Node QtQuick3D::View3D::importScene |
407 | |
408 | This property defines the reference node of the scene to render to the |
409 | viewport. The node does not have to be a child of the View3D. This |
410 | referenced node becomes a sibling with child nodes of View3D, if there are |
411 | any. |
412 | |
413 | \note This property can only be set once, and subsequent changes will have |
414 | no effect. |
415 | |
416 | You can also define a hierarchy of \l{Node}{Node-based} items as children of |
417 | the View3D directly, then this will become the implicit scene of the |
418 | View3D. |
419 | |
420 | To return the current scene of the View3D, use the \l scene property. |
421 | |
422 | \sa Node |
423 | */ |
424 | QQuick3DNode *QQuick3DViewport::importScene() const |
425 | { |
426 | return m_importScene; |
427 | } |
428 | |
429 | /*! |
430 | \qmlproperty enumeration QtQuick3D::View3D::renderMode |
431 | |
432 | This property determines how the View3D is combined with the other parts of the |
433 | Qt Quick scene. |
434 | |
435 | By default, the scene will be rendered into an off-screen buffer as an intermediate |
436 | step. This off-screen buffer is then rendered into the window (or render target) like any |
437 | other Qt Quick \l Item. |
438 | |
439 | For most users, there will be no need to change the render mode, and this property can |
440 | safely be ignored. But on some graphics hardware, the use of an off-screen buffer can be |
441 | a performance bottleneck. If this is the case, it might be worth experimenting with other |
442 | modes. |
443 | |
444 | \value View3D.Offscreen The scene is rendered into an off-screen buffer as an intermediate |
445 | step. This off-screen buffer is then composited with the rest of the Qt Quick scene. |
446 | |
447 | \value View3D.Underlay The scene is rendered directly to the window before the rest of |
448 | the Qt Quick scene is rendered. With this mode, the View3D cannot be placed on top of |
449 | other Qt Quick items. |
450 | |
451 | \value View3D.Overlay The scene is rendered directly to the window after Qt Quick is |
452 | rendered. With this mode, the View3D will always be on top of other Qt Quick items. |
453 | |
454 | \value View3D.Inline The View3D's scene graph is embedded into the main scene graph, |
455 | and the same ordering semantics are applied as to any other Qt Quick \l Item. As this |
456 | mode can lead to subtle issues, depending on the contents of the scene, due to |
457 | injecting depth-based 3D content into a 2D scene graph, it is not recommended to be |
458 | used, unless a specific need arises. |
459 | |
460 | The default is \c{View3D.Offscreen}. |
461 | |
462 | \note When changing the render mode, it is important to note that \c{View3D.Offscreen} (the |
463 | default) is the only mode which guarantees perfect graphics fidelity. The other modes |
464 | all have limitations that can cause visual glitches, so it is important to check that |
465 | the visual output still looks correct when changing this property. |
466 | |
467 | \note When using the Underlay, Overlay, or Inline modes, it can be useful, and in some |
468 | cases, required, to disable the Qt Quick scene graph's depth buffer writes via |
469 | QQuickGraphicsConfiguration::setDepthBufferFor2D() before showing the QQuickWindow or |
470 | QQuickView hosting the View3D item. |
471 | */ |
472 | QQuick3DViewport::RenderMode QQuick3DViewport::renderMode() const |
473 | { |
474 | return m_renderMode; |
475 | } |
476 | |
477 | /*! |
478 | \qmlproperty enumeration QtQuick3D::View3D::renderFormat |
479 | \since 6.4 |
480 | |
481 | This property determines the backing texture's format. Applicable only when |
482 | the View3D is rendering to a texture, for example because the \l renderMode |
483 | is \c{View3D.Offscreen}. |
484 | |
485 | The default is \c{ShaderEffectSource.RGBA8}. |
486 | |
487 | If the format is not supported by the underlying graphics driver at run |
488 | time, RGBA8 is used. |
489 | |
490 | \list |
491 | \li ShaderEffectSource.RGBA8 |
492 | \li ShaderEffectSource.RGBA16F |
493 | \li ShaderEffectSource.RGBA32F |
494 | \endlist |
495 | |
496 | \sa QtQuick::ShaderEffectSource::format, QtQuick::Item::layer.format |
497 | */ |
498 | #if QT_CONFIG(quick_shadereffect) |
499 | QQuickShaderEffectSource::Format QQuick3DViewport::renderFormat() const |
500 | { |
501 | return m_renderFormat; |
502 | } |
503 | #endif |
504 | |
505 | /*! |
506 | \qmlproperty QtQuick3D::RenderStats QtQuick3D::View3D::renderStats |
507 | \readonly |
508 | |
509 | This property provides statistics about the rendering of a frame, such as |
510 | \l {RenderStats::fps}{fps}, \l {RenderStats::frameTime}{frameTime}, |
511 | \l {RenderStats::renderTime}{renderTime}, \l {RenderStats::syncTime}{syncTime}, |
512 | and \l {RenderStats::maxFrameTime}{maxFrameTime}. |
513 | */ |
514 | QQuick3DRenderStats *QQuick3DViewport::renderStats() const |
515 | { |
516 | return m_renderStats; |
517 | } |
518 | |
519 | QQuick3DSceneRenderer *QQuick3DViewport::createRenderer() const |
520 | { |
521 | QQuick3DSceneRenderer *renderer = nullptr; |
522 | |
523 | if (QQuickWindow *qw = window()) { |
524 | auto wa = QQuick3DSceneManager::getOrSetWindowAttachment(window&: *qw); |
525 | auto rci = wa->rci(); |
526 | if (!rci) { |
527 | QSGRendererInterface *rif = qw->rendererInterface(); |
528 | if (QSSG_GUARD(QSGRendererInterface::isApiRhiBased(rif->graphicsApi()))) { |
529 | QRhi *rhi = static_cast<QRhi *>(rif->getResource(window: qw, resource: QSGRendererInterface::RhiResource)); |
530 | QSSG_CHECK_X(rhi != nullptr, "No QRhi from QQuickWindow, this cannot happen" ); |
531 | // The RenderContextInterface, and the objects owned by it (such |
532 | // as, the BufferManager) are always per-QQuickWindow, and so per |
533 | // scenegraph render thread. Hence the association with window. |
534 | // Multiple View3Ds in the same window can use the same rendering |
535 | // infrastructure (so e.g. the same QSSGBufferManager), but two |
536 | // View3D objects in different windows must not, except for certain |
537 | // components that do not work with and own native graphics |
538 | // resources (most notably, QSSGShaderLibraryManager - but this |
539 | // distinction is handled internally by QSSGRenderContextInterface). |
540 | rci = std::make_shared<QSSGRenderContextInterface>(args&: rhi); |
541 | wa->setRci(rci); |
542 | |
543 | // Use DirectConnection to stay on the render thread, if there is one. |
544 | connect(sender: wa, signal: &QQuick3DWindowAttachment::releaseCachedResources, context: this, |
545 | slot: &QQuick3DViewport::onReleaseCachedResources, type: Qt::DirectConnection); |
546 | |
547 | } else { |
548 | qWarning(msg: "The Qt Quick scene is using a rendering method that is not based on QRhi and a 3D graphics API. " |
549 | "Qt Quick 3D is not functional in such an environment. The View3D item is not going to display anything." ); |
550 | } |
551 | } |
552 | |
553 | if (rci) |
554 | renderer = new QQuick3DSceneRenderer(rci); |
555 | } |
556 | |
557 | return renderer; |
558 | } |
559 | |
560 | bool QQuick3DViewport::isTextureProvider() const |
561 | { |
562 | // We can only be a texture provider if we are rendering to a texture first |
563 | if (m_renderMode == QQuick3DViewport::Offscreen) |
564 | return true; |
565 | |
566 | return false; |
567 | } |
568 | |
569 | QSGTextureProvider *QQuick3DViewport::textureProvider() const |
570 | { |
571 | // When Item::layer::enabled == true, QQuickItem will be a texture |
572 | // provider. In this case we should prefer to return the layer rather |
573 | // than the fbo texture. |
574 | if (QQuickItem::isTextureProvider()) |
575 | return QQuickItem::textureProvider(); |
576 | |
577 | // We can only be a texture provider if we are rendering to a texture first |
578 | if (m_renderMode != QQuick3DViewport::Offscreen) |
579 | return nullptr; |
580 | |
581 | QQuickWindow *w = window(); |
582 | if (!w) { |
583 | qWarning(msg: "QSSGView3D::textureProvider: can only be queried on the rendering thread of an exposed window" ); |
584 | return nullptr; |
585 | } |
586 | |
587 | if (!m_node) |
588 | m_node = new SGFramebufferObjectNode; |
589 | return m_node; |
590 | } |
591 | |
592 | class CleanupJob : public QRunnable |
593 | { |
594 | public: |
595 | CleanupJob(QQuick3DSGDirectRenderer *renderer) : m_renderer(renderer) { } |
596 | void run() override { delete m_renderer; } |
597 | private: |
598 | QQuick3DSGDirectRenderer *m_renderer; |
599 | }; |
600 | |
601 | void QQuick3DViewport::releaseResources() |
602 | { |
603 | if (m_directRenderer) { |
604 | window()->scheduleRenderJob(job: new CleanupJob(m_directRenderer), schedule: QQuickWindow::BeforeSynchronizingStage); |
605 | m_directRenderer = nullptr; |
606 | } |
607 | |
608 | m_node = nullptr; |
609 | } |
610 | |
611 | void QQuick3DViewport::cleanupDirectRenderer() |
612 | { |
613 | delete m_directRenderer; |
614 | m_directRenderer = nullptr; |
615 | } |
616 | |
617 | void QQuick3DViewport::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) |
618 | { |
619 | QQuickItem::geometryChange(newGeometry, oldGeometry); |
620 | |
621 | if (newGeometry.size() != oldGeometry.size()) |
622 | update(); |
623 | } |
624 | |
625 | QSGNode *QQuick3DViewport::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *) |
626 | { |
627 | // When changing render modes |
628 | if (m_renderModeDirty) { |
629 | if (node) { |
630 | delete node; |
631 | node = nullptr; |
632 | m_node = nullptr; |
633 | m_renderNode = nullptr; |
634 | } |
635 | if (m_directRenderer) { |
636 | delete m_directRenderer; |
637 | m_directRenderer = nullptr; |
638 | } |
639 | } |
640 | |
641 | m_renderModeDirty = false; |
642 | |
643 | switch (m_renderMode) { |
644 | // Direct rendering |
645 | case Underlay: |
646 | Q_FALLTHROUGH(); |
647 | case Overlay: |
648 | setupDirectRenderer(m_renderMode); |
649 | node = nullptr; |
650 | break; |
651 | case Offscreen: |
652 | node = setupOffscreenRenderer(node); |
653 | break; |
654 | case Inline: |
655 | // QSGRenderNode-based rendering |
656 | node = setupInlineRenderer(node); |
657 | break; |
658 | } |
659 | |
660 | if (!isforceInputHandlingSet()) { |
661 | // Implicitly enable internal input processing if any item2ds are present. |
662 | const auto inputHandlingEnabled = |
663 | QQuick3DObjectPrivate::get(item: m_sceneRoot)->sceneManager->inputHandlingEnabled; |
664 | const auto enable = inputHandlingEnabled > 0; |
665 | if (m_enableInputProcessing != enable) { |
666 | m_enableInputProcessing = enable; |
667 | QMetaObject::invokeMethod(obj: this, member: "updateInputProcessing" , c: Qt::QueuedConnection); |
668 | } |
669 | } |
670 | |
671 | return node; |
672 | } |
673 | |
674 | void QQuick3DViewport::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) |
675 | { |
676 | if (change == ItemSceneChange) { |
677 | if (value.window) { |
678 | // TODO: if we want to support multiple windows, there has to be a scene manager for |
679 | // every window. |
680 | QQuick3DObjectPrivate::get(item: m_sceneRoot)->sceneManager->setWindow(value.window); |
681 | if (m_importScene) |
682 | QQuick3DObjectPrivate::get(item: m_importScene)->sceneManager->setWindow(value.window); |
683 | m_renderStats->setWindow(value.window); |
684 | } |
685 | } else if (change == ItemVisibleHasChanged && isVisible()) { |
686 | update(); |
687 | } |
688 | } |
689 | |
690 | bool QQuick3DViewport::event(QEvent *event) |
691 | { |
692 | if (m_enableInputProcessing && event->isPointerEvent()) |
693 | return internalPick(event: static_cast<QPointerEvent *>(event)); |
694 | else |
695 | return QQuickItem::event(event); |
696 | } |
697 | |
698 | void QQuick3DViewport::componentComplete() |
699 | { |
700 | QQuickItem::componentComplete(); |
701 | Q_QUICK3D_PROFILE_REGISTER(this); |
702 | } |
703 | |
704 | void QQuick3DViewport::setCamera(QQuick3DCamera *camera) |
705 | { |
706 | if (m_camera == camera) |
707 | return; |
708 | |
709 | if (camera && !camera->parentItem()) |
710 | camera->setParentItem(m_sceneRoot); |
711 | if (camera) |
712 | camera->updateGlobalVariables(inViewport: QRect(0, 0, width(), height())); |
713 | |
714 | QQuick3DObjectPrivate::attachWatcherPriv(sceneContext: m_sceneRoot, callContext: this, setter: &QQuick3DViewport::setCamera, newO: camera, oldO: m_camera); |
715 | |
716 | m_camera = camera; |
717 | emit cameraChanged(); |
718 | update(); |
719 | } |
720 | |
721 | void QQuick3DViewport::setMultiViewCameras(QQuick3DCamera **firstCamera, int count) |
722 | { |
723 | m_multiViewCameras.clear(); |
724 | bool sendChangeSignal = false; |
725 | for (int i = 0; i < count; ++i) { |
726 | QQuick3DCamera *camera = *(firstCamera + i); |
727 | if (camera) { |
728 | if (!camera->parentItem()) |
729 | camera->setParentItem(m_sceneRoot); |
730 | camera->updateGlobalVariables(inViewport: QRect(0, 0, width(), height())); |
731 | } |
732 | if (i == 0) { |
733 | if (m_camera != camera) { |
734 | m_camera = camera; |
735 | sendChangeSignal = true; |
736 | } |
737 | } |
738 | |
739 | m_multiViewCameras.append(t: camera); |
740 | |
741 | // ### do we need attachWatcher stuff? the Xr-provided cameras cannot disappear, although the XrActor (the owner) might |
742 | } |
743 | |
744 | if (sendChangeSignal) |
745 | emit cameraChanged(); |
746 | |
747 | update(); |
748 | } |
749 | |
750 | void QQuick3DViewport::setEnvironment(QQuick3DSceneEnvironment *environment) |
751 | { |
752 | if (m_environment == environment) |
753 | return; |
754 | |
755 | m_environment = environment; |
756 | if (m_environment && !m_environment->parentItem()) |
757 | m_environment->setParentItem(m_sceneRoot); |
758 | emit environmentChanged(); |
759 | update(); |
760 | } |
761 | |
762 | void QQuick3DViewport::setImportScene(QQuick3DNode *inScene) |
763 | { |
764 | // ### We may need consider the case where there is |
765 | // already a scene tree here |
766 | // FIXME : Only the first importScene is an effective one |
767 | if (m_importScene) |
768 | return; |
769 | |
770 | // FIXME : Check self-import or cross-import |
771 | // Currently it does not work since importScene qml parsed in a reverse order. |
772 | QQuick3DNode *scene = inScene; |
773 | while (scene) { |
774 | if (m_sceneRoot == scene) { |
775 | qmlWarning(me: this) << "Cannot allow self-import or cross-import!" ; |
776 | return; |
777 | } |
778 | |
779 | QQuick3DSceneRootNode *rn = qobject_cast<QQuick3DSceneRootNode *>(object: scene); |
780 | scene = rn ? rn->view3D()->importScene() : nullptr; |
781 | } |
782 | |
783 | m_importScene = inScene; |
784 | if (m_importScene) { |
785 | auto privateObject = QQuick3DObjectPrivate::get(item: m_importScene); |
786 | if (!privateObject->sceneManager) { |
787 | // If object doesn't already have scene manager, check from its children |
788 | QQuick3DSceneManager *manager = findChildSceneManager(inObject: m_importScene); |
789 | // If still not found, use the one from the scene root (scenes defined outside of an view3d) |
790 | if (!manager) |
791 | manager = QQuick3DObjectPrivate::get(item: m_sceneRoot)->sceneManager; |
792 | if (manager) { |
793 | manager->setWindow(window()); |
794 | privateObject->refSceneManager(*manager); |
795 | } |
796 | // At this point some manager will exist |
797 | Q_ASSERT(privateObject->sceneManager); |
798 | } |
799 | |
800 | connect(sender: privateObject->sceneManager, signal: &QQuick3DSceneManager::needsUpdate, |
801 | context: this, slot: &QQuickItem::update); |
802 | |
803 | QQuick3DNode *scene = inScene; |
804 | while (scene) { |
805 | QQuick3DSceneRootNode *rn = qobject_cast<QQuick3DSceneRootNode *>(object: scene); |
806 | scene = rn ? rn->view3D()->importScene() : nullptr; |
807 | |
808 | if (scene) { |
809 | connect(sender: QQuick3DObjectPrivate::get(item: scene)->sceneManager, |
810 | signal: &QQuick3DSceneManager::needsUpdate, |
811 | context: this, slot: &QQuickItem::update); |
812 | } |
813 | } |
814 | } |
815 | |
816 | emit importSceneChanged(); |
817 | update(); |
818 | } |
819 | |
820 | void QQuick3DViewport::setRenderMode(QQuick3DViewport::RenderMode renderMode) |
821 | { |
822 | if (m_renderMode == renderMode) |
823 | return; |
824 | |
825 | m_renderMode = renderMode; |
826 | m_renderModeDirty = true; |
827 | emit renderModeChanged(); |
828 | update(); |
829 | } |
830 | |
831 | #if QT_CONFIG(quick_shadereffect) |
832 | void QQuick3DViewport::setRenderFormat(QQuickShaderEffectSource::Format format) |
833 | { |
834 | if (m_renderFormat == format) |
835 | return; |
836 | |
837 | m_renderFormat = format; |
838 | m_renderModeDirty = true; |
839 | emit renderFormatChanged(); |
840 | update(); |
841 | } |
842 | #endif |
843 | |
844 | /*! |
845 | \qmlproperty int QtQuick3D::View3D::explicitTextureWidth |
846 | \since 6.7 |
847 | |
848 | The width, in pixels, of the item's associated texture. Relevant when a |
849 | fixed texture size is desired that does not depend on the item's size. This |
850 | size has no effect on the geometry of the item (its size and placement |
851 | within the scene), which means the texture's content will appear scaled up |
852 | or down (and possibly stretched) onto the item's area. |
853 | |
854 | By default the value is \c 0. A value of 0 means that texture's size |
855 | follows the item's size. (\c{texture size in pixels} = \c{item's logical |
856 | size} * \c{device pixel ratio}). |
857 | |
858 | \note This property is relevant only when \l renderMode is set to \c |
859 | Offscreen. Its value is ignored otherwise. |
860 | |
861 | \sa explicitTextureHeight, effectiveTextureSize, DebugView |
862 | */ |
863 | int QQuick3DViewport::explicitTextureWidth() const |
864 | { |
865 | return m_explicitTextureWidth; |
866 | } |
867 | |
868 | void QQuick3DViewport::setExplicitTextureWidth(int width) |
869 | { |
870 | if (m_explicitTextureWidth == width) |
871 | return; |
872 | |
873 | m_explicitTextureWidth = width; |
874 | emit explicitTextureWidthChanged(); |
875 | update(); |
876 | } |
877 | |
878 | /*! |
879 | \qmlproperty int QtQuick3D::View3D::explicitTextureHeight |
880 | \since 6.7 |
881 | |
882 | The height, in pixels, of the item's associated texture. Relevant when a |
883 | fixed texture size is desired that does not depend on the item's size. This |
884 | size has no effect on the geometry of the item (its size and placement |
885 | within the scene), which means the texture's content will appear scaled up |
886 | or down (and possibly stretched) onto the item's area. |
887 | |
888 | By default the value is \c 0. A value of 0 means that texture's size |
889 | follows the item's size. (\c{texture size in pixels} = \c{item's logical |
890 | size} * \c{device pixel ratio}). |
891 | |
892 | \note This property is relevant only when \l renderMode is set to \c |
893 | Offscreen. Its value is ignored otherwise. |
894 | |
895 | \sa explicitTextureWidth, effectiveTextureSize, DebugView |
896 | */ |
897 | int QQuick3DViewport::explicitTextureHeight() const |
898 | { |
899 | return m_explicitTextureHeight; |
900 | } |
901 | |
902 | void QQuick3DViewport::setExplicitTextureHeight(int height) |
903 | { |
904 | if (m_explicitTextureHeight == height) |
905 | return; |
906 | |
907 | m_explicitTextureHeight = height; |
908 | emit explicitTextureHeightChanged(); |
909 | update(); |
910 | } |
911 | |
912 | /*! |
913 | \qmlproperty size QtQuick3D::View3D::effectiveTextureSize |
914 | \since 6.7 |
915 | |
916 | This property exposes the size, in pixels, of the underlying color (and |
917 | depth/stencil) buffers. It is provided for use on the GUI (main) thread, in |
918 | QML bindings or JavaScript. |
919 | |
920 | This is a read-only property. |
921 | |
922 | \note This property is relevant only when \l renderMode is set to |
923 | \c Offscreen. |
924 | |
925 | \sa explicitTextureWidth, explicitTextureHeight, DebugView |
926 | */ |
927 | QSize QQuick3DViewport::effectiveTextureSize() const |
928 | { |
929 | return m_effectiveTextureSize; |
930 | } |
931 | |
932 | |
933 | /*! |
934 | \qmlmethod vector3d View3D::mapFrom3DScene(vector3d scenePos) |
935 | |
936 | Transforms \a scenePos from scene space (3D) into view space (2D). |
937 | |
938 | The returned x- and y-values will be be in view coordinates, with the top-left |
939 | corner at [0, 0] and the bottom-right corner at [width, height]. The returned |
940 | z-value contains the distance from the near clip plane of the frustum (clipNear) to |
941 | \a scenePos in scene coordinates. If the distance is negative, that means the \a scenePos |
942 | is behind the active camera. If \a scenePos cannot be mapped to a position in the scene, |
943 | a position of [0, 0, 0] is returned. |
944 | |
945 | This function requires that \l camera is assigned to the view. |
946 | |
947 | \sa mapTo3DScene(), {Camera::mapToViewport()}{Camera.mapToViewport()} |
948 | */ |
949 | QVector3D QQuick3DViewport::mapFrom3DScene(const QVector3D &scenePos) const |
950 | { |
951 | if (!m_camera) { |
952 | qmlWarning(me: this) << "Cannot resolve view position without a camera assigned!" ; |
953 | return QVector3D(0, 0, 0); |
954 | } |
955 | |
956 | qreal _width = width(); |
957 | qreal _height = height(); |
958 | if (_width == 0 || _height == 0) |
959 | return QVector3D(0, 0, 0); |
960 | |
961 | const QVector3D normalizedPos = m_camera->mapToViewport(scenePos, width: _width, height: _height); |
962 | return normalizedPos * QVector3D(float(_width), float(_height), 1); |
963 | } |
964 | |
965 | /*! |
966 | \qmlmethod vector3d View3D::mapTo3DScene(vector3d viewPos) |
967 | |
968 | Transforms \a viewPos from view space (2D) into scene space (3D). |
969 | |
970 | The x- and y-values of \a viewPos should be in view coordinates, with the top-left |
971 | corner at [0, 0] and the bottom-right corner at [width, height]. The z-value is |
972 | interpreted as the distance from the near clip plane of the frustum (clipNear) in |
973 | scene coordinates. |
974 | |
975 | If \a viewPos cannot successfully be mapped to a position in the scene, a position of |
976 | [0, 0, 0] is returned. |
977 | |
978 | This function requires that a \l camera is assigned to the view. |
979 | |
980 | \sa mapFrom3DScene(), {Camera::mapFromViewport}{Camera.mapFromViewport()} |
981 | */ |
982 | QVector3D QQuick3DViewport::mapTo3DScene(const QVector3D &viewPos) const |
983 | { |
984 | if (!m_camera) { |
985 | qmlWarning(me: this) << "Cannot resolve scene position without a camera assigned!" ; |
986 | return QVector3D(0, 0, 0); |
987 | } |
988 | |
989 | qreal _width = width(); |
990 | qreal _height = height(); |
991 | if (_width == 0 || _height == 0) |
992 | return QVector3D(0, 0, 0); |
993 | |
994 | const QVector3D normalizedPos = viewPos / QVector3D(float(_width), float(_height), 1); |
995 | return m_camera->mapFromViewport(viewportPos: normalizedPos, width: _width, height: _height); |
996 | } |
997 | |
998 | /*! |
999 | \qmlmethod pickResult View3D::pick(float x, float y) |
1000 | |
1001 | This method will "shoot" a ray into the scene from view coordinates \a x and \a y |
1002 | and return information about the nearest intersection with an object in the scene. |
1003 | |
1004 | This can, for instance, be called with mouse coordinates to find the object under the mouse cursor. |
1005 | */ |
1006 | QQuick3DPickResult QQuick3DViewport::pick(float x, float y) const |
1007 | { |
1008 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1009 | if (!renderer) |
1010 | return QQuick3DPickResult(); |
1011 | |
1012 | const QPointF position(qreal(x) * window()->effectiveDevicePixelRatio() * m_widthMultiplier, |
1013 | qreal(y) * window()->effectiveDevicePixelRatio() * m_heightMultiplier); |
1014 | std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(pos: position); |
1015 | if (!rayResult.has_value()) |
1016 | return QQuick3DPickResult(); |
1017 | |
1018 | const auto resultList = renderer->syncPick(ray: rayResult.value()); |
1019 | return getNearestPickResult(pickResults: resultList); |
1020 | } |
1021 | |
1022 | /*! |
1023 | \qmlmethod pickResult View3D::pick(float x, float y, Model model) |
1024 | |
1025 | This method will "shoot" a ray into the scene from view coordinates \a x and \a y |
1026 | and return information about the intersection between the ray and the specified \a model. |
1027 | |
1028 | This can, for instance, be called with mouse coordinates to find the object under the mouse cursor. |
1029 | |
1030 | \since 6.8 |
1031 | */ |
1032 | QQuick3DPickResult QQuick3DViewport::pick(float x, float y, QQuick3DModel *model) const |
1033 | { |
1034 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1035 | if (!renderer) |
1036 | return QQuick3DPickResult(); |
1037 | |
1038 | const QPointF position(qreal(x) * window()->effectiveDevicePixelRatio(), |
1039 | qreal(y) * window()->effectiveDevicePixelRatio()); |
1040 | std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(pos: position); |
1041 | |
1042 | if (!rayResult.has_value()) |
1043 | return QQuick3DPickResult(); |
1044 | |
1045 | const auto renderNode = static_cast<QSSGRenderNode *>(QQuick3DObjectPrivate::get(item: model)->spatialNode); |
1046 | const auto resultList = renderer->syncPickOne(ray: rayResult.value(), node: renderNode); |
1047 | return getNearestPickResult(pickResults: resultList); |
1048 | } |
1049 | |
1050 | /*! |
1051 | \qmlmethod List<pickResult> View3D::pickSubset(float x, float y, list<Model> models) |
1052 | |
1053 | This method will "shoot" a ray into the scene from view coordinates \a x and \a y |
1054 | and return information about the intersections with the passed in list of \a models. |
1055 | This will only check against the list of models passed in. |
1056 | The returned list is sorted by distance from the camera with the nearest |
1057 | intersections appearing first and the furthest appearing last. |
1058 | |
1059 | This can, for instance, be called with mouse coordinates to find the object under the mouse cursor. |
1060 | |
1061 | Works with both property list<Model> and dynamic JavaScript arrays of models. |
1062 | |
1063 | \since 6.8 |
1064 | */ |
1065 | QList<QQuick3DPickResult> QQuick3DViewport::pickSubset(float x, float y, const QJSValue &models) const |
1066 | { |
1067 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1068 | if (!renderer) |
1069 | return {}; |
1070 | |
1071 | QVarLengthArray<QSSGRenderNode*> renderNodes; |
1072 | // Check for regular JavaScript array |
1073 | if (models.isArray()) { |
1074 | const auto length = models.property(QStringLiteral("length" )).toInt(); |
1075 | if (length == 0) |
1076 | return {}; |
1077 | |
1078 | for (int i = 0; i < length; ++i) { |
1079 | const auto isQObject = models.property(arrayIndex: i).isQObject(); |
1080 | if (!isQObject) { |
1081 | qmlWarning(me: this) << "Type provided for picking is not a QObject. Needs to be of type QQuick3DModel." ; |
1082 | continue; |
1083 | } |
1084 | const auto obj = models.property(arrayIndex: i).toQObject(); |
1085 | const auto model = qobject_cast<QQuick3DModel *>(object: obj); |
1086 | if (!model) { |
1087 | qmlWarning(me: this) << "Type " << obj->metaObject()->className() << " is not supported for picking. Needs to be of type QQuick3DModel." ; |
1088 | continue; |
1089 | } |
1090 | const auto priv = QQuick3DObjectPrivate::get(item: model); |
1091 | if (priv && priv->spatialNode) { |
1092 | renderNodes.push_back(t: static_cast<QSSGRenderNode*>(priv->spatialNode)); |
1093 | } |
1094 | } |
1095 | } else { |
1096 | // Check for property list<Model> |
1097 | const auto subsetVariant = models.toVariant(); |
1098 | if (!subsetVariant.isValid() || !subsetVariant.canConvert<QQmlListReference>()) |
1099 | return {}; |
1100 | |
1101 | const auto list = subsetVariant.value<QQmlListReference>(); |
1102 | |
1103 | // Only support array of models |
1104 | if (list.listElementType()->className() != QQuick3DModel::staticMetaObject.className()) { |
1105 | qmlWarning(me: this) << "Type " << list.listElementType()->className() << " is not supported for picking. Needs to be of type QQuick3DModel." ; |
1106 | return {}; |
1107 | } |
1108 | for (int i = 0; i < list.count(); ++i) { |
1109 | auto model = static_cast<QQuick3DModel *>(list.at(i)); |
1110 | if (!model) |
1111 | continue; |
1112 | auto priv = QQuick3DObjectPrivate::get(item: model); |
1113 | if (priv && priv->spatialNode) { |
1114 | renderNodes.push_back(t: static_cast<QSSGRenderNode*>(priv->spatialNode)); |
1115 | } |
1116 | } |
1117 | } |
1118 | |
1119 | if (renderNodes.empty()) |
1120 | return {}; |
1121 | |
1122 | const QPointF position(qreal(x) * window()->effectiveDevicePixelRatio(), |
1123 | qreal(y) * window()->effectiveDevicePixelRatio()); |
1124 | std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(pos: position); |
1125 | if (!rayResult.has_value()) |
1126 | return {}; |
1127 | |
1128 | const auto resultList = renderer->syncPickSubset(ray: rayResult.value(), subset: renderNodes); |
1129 | |
1130 | QList<QQuick3DPickResult> processedResultList; |
1131 | processedResultList.reserve(asize: resultList.size()); |
1132 | for (const auto &result : resultList) |
1133 | processedResultList.append(t: processPickResult(pickResult: result)); |
1134 | |
1135 | return processedResultList; |
1136 | } |
1137 | |
1138 | /*! |
1139 | \qmlmethod List<pickResult> View3D::pickAll(float x, float y) |
1140 | |
1141 | This method will "shoot" a ray into the scene from view coordinates \a x and \a y |
1142 | and return a list of information about intersections with objects in the scene. |
1143 | The returned list is sorted by distance from the camera with the nearest |
1144 | intersections appearing first and the furthest appearing last. |
1145 | |
1146 | This can, for instance, be called with mouse coordinates to find the object under the mouse cursor. |
1147 | |
1148 | \since 6.2 |
1149 | */ |
1150 | QList<QQuick3DPickResult> QQuick3DViewport::pickAll(float x, float y) const |
1151 | { |
1152 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1153 | if (!renderer) |
1154 | return QList<QQuick3DPickResult>(); |
1155 | |
1156 | const QPointF position(qreal(x) * window()->effectiveDevicePixelRatio() * m_widthMultiplier, |
1157 | qreal(y) * window()->effectiveDevicePixelRatio() * m_heightMultiplier); |
1158 | std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(pos: position); |
1159 | if (!rayResult.has_value()) |
1160 | return QList<QQuick3DPickResult>(); |
1161 | |
1162 | const auto resultList = renderer->syncPickAll(ray: rayResult.value()); |
1163 | QList<QQuick3DPickResult> processedResultList; |
1164 | processedResultList.reserve(asize: resultList.size()); |
1165 | for (const auto &result : resultList) |
1166 | processedResultList.append(t: processPickResult(pickResult: result)); |
1167 | |
1168 | return processedResultList; |
1169 | } |
1170 | |
1171 | /*! |
1172 | \qmlmethod pickResult View3D::rayPick(vector3d origin, vector3d direction) |
1173 | |
1174 | This method will "shoot" a ray into the scene starting at \a origin and in |
1175 | \a direction and return information about the nearest intersection with an |
1176 | object in the scene. |
1177 | |
1178 | This can, for instance, be called with the position and forward vector of |
1179 | any object in a scene to see what object is in front of an item. This |
1180 | makes it possible to do picking from any point in the scene. |
1181 | |
1182 | \since 6.2 |
1183 | */ |
1184 | QQuick3DPickResult QQuick3DViewport::rayPick(const QVector3D &origin, const QVector3D &direction) const |
1185 | { |
1186 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1187 | if (!renderer) |
1188 | return QQuick3DPickResult(); |
1189 | |
1190 | const QSSGRenderRay ray(origin, direction); |
1191 | const auto resultList = renderer->syncPick(ray); |
1192 | return getNearestPickResult(pickResults: resultList); |
1193 | } |
1194 | |
1195 | /*! |
1196 | \qmlmethod List<pickResult> View3D::rayPickAll(vector3d origin, vector3d direction) |
1197 | |
1198 | This method will "shoot" a ray into the scene starting at \a origin and in |
1199 | \a direction and return a list of information about the nearest intersections with |
1200 | objects in the scene. |
1201 | The list is presorted by distance from the origin along the direction |
1202 | vector with the nearest intersections appearing first and the furthest |
1203 | appearing last. |
1204 | |
1205 | This can, for instance, be called with the position and forward vector of |
1206 | any object in a scene to see what objects are in front of an item. This |
1207 | makes it possible to do picking from any point in the scene. |
1208 | |
1209 | \since 6.2 |
1210 | */ |
1211 | QList<QQuick3DPickResult> QQuick3DViewport::rayPickAll(const QVector3D &origin, const QVector3D &direction) const |
1212 | { |
1213 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1214 | if (!renderer) |
1215 | return QList<QQuick3DPickResult>(); |
1216 | |
1217 | const QSSGRenderRay ray(origin, direction); |
1218 | |
1219 | const auto resultList = renderer->syncPickAll(ray); |
1220 | QList<QQuick3DPickResult> processedResultList; |
1221 | processedResultList.reserve(asize: resultList.size()); |
1222 | for (const auto &result : resultList) { |
1223 | auto processedResult = processPickResult(pickResult: result); |
1224 | if (processedResult.hitType() != QQuick3DPickResultEnums::HitType::Null) |
1225 | processedResultList.append(t: processedResult); |
1226 | } |
1227 | |
1228 | return processedResultList; |
1229 | } |
1230 | |
1231 | void QQuick3DViewport::processPointerEventFromRay(const QVector3D &origin, const QVector3D &direction, QPointerEvent *event) |
1232 | { |
1233 | internalPick(event, origin, direction); |
1234 | } |
1235 | |
1236 | // Note: we have enough information to implement Capability::Hover and Capability::ZPosition, |
1237 | // but those properties are not currently available in QTouchEvent/QEventPoint |
1238 | |
1239 | namespace { |
1240 | class SyntheticTouchDevice : public QPointingDevice |
1241 | { |
1242 | public: |
1243 | SyntheticTouchDevice(QObject *parent = nullptr) |
1244 | : QPointingDevice(QLatin1StringView("QtQuick3D Touch Synthesizer" ), |
1245 | 0, |
1246 | DeviceType::TouchScreen, |
1247 | PointerType::Finger, |
1248 | Capability::Position, |
1249 | 10, 0, |
1250 | QString(), QPointingDeviceUniqueId(), |
1251 | parent) |
1252 | { |
1253 | } |
1254 | }; |
1255 | } |
1256 | |
1257 | /*! |
1258 | \qmlmethod View3D::setTouchpoint(Item target, point position, int pointId, bool pressed) |
1259 | |
1260 | Sends a synthetic touch event to \a target, moving the touch point with ID \a pointId to \a position, |
1261 | with \a pressed determining if the point is pressed. |
1262 | Also sends the appropriate touch release event if \a pointId was previously active on a different |
1263 | item. |
1264 | |
1265 | \since 6.8 |
1266 | */ |
1267 | |
1268 | void QQuick3DViewport::setTouchpoint(QQuickItem *target, const QPointF &position, int pointId, bool pressed) |
1269 | { |
1270 | if (pointId >= m_touchState.size()) |
1271 | m_touchState.resize(sz: pointId + 1); |
1272 | auto prevState = m_touchState[pointId]; |
1273 | |
1274 | const bool sameTarget = prevState.target == target; |
1275 | const bool wasPressed = prevState.isPressed; |
1276 | |
1277 | const bool isPress = pressed && (!sameTarget || !wasPressed); |
1278 | const bool isRelease = !pressed && wasPressed && sameTarget; |
1279 | |
1280 | // Hover if we're not active, and we weren't previously active. |
1281 | // We assume that we always get a non-active for a target when we release. |
1282 | // This function sends a release events if the target is changed. |
1283 | if (!sameTarget && wasPressed) |
1284 | qWarning(msg: "QQuick3DViewport::setTouchpoint missing release event" ); |
1285 | |
1286 | if (!pressed && !wasPressed) { |
1287 | // This would be a hover event: skipping |
1288 | return; |
1289 | } |
1290 | |
1291 | m_touchState[pointId] = { .target: target, .position: position, .isPressed: pressed }; |
1292 | |
1293 | if (!m_syntheticTouchDevice) |
1294 | m_syntheticTouchDevice = new SyntheticTouchDevice(this); |
1295 | |
1296 | QPointingDevicePrivate *devPriv = QPointingDevicePrivate::get(q: m_syntheticTouchDevice); |
1297 | |
1298 | auto makePoint = [devPriv](int id, QEventPoint::State pointState, QPointF pos) -> QEventPoint { |
1299 | auto epd = devPriv->pointById(id); |
1300 | auto &ep = epd->eventPoint; |
1301 | if (pointState != QEventPoint::State::Stationary) |
1302 | ep.setAccepted(false); |
1303 | |
1304 | auto res = QMutableEventPoint::withTimeStamp(timestamp: 0, pointId: id, state: pointState, position: pos, scenePosition: pos, globalPosition: pos); |
1305 | QMutableEventPoint::update(from: res, to&: ep); |
1306 | return res; |
1307 | }; |
1308 | |
1309 | auto sendTouchEvent = [&](QQuickItem *t, const QPointF &position, int pointId, QEventPoint::State pointState) -> void { |
1310 | QList<QEventPoint> points; |
1311 | bool otherPoint = false; // Does the event have another point already? |
1312 | for (int i = 0; i < m_touchState.size(); ++i) { |
1313 | const auto &ts = m_touchState[i]; |
1314 | if (ts.target != t) |
1315 | continue; |
1316 | if (i == pointId) { |
1317 | auto newPoint = makePoint(i, pointState, position); |
1318 | points << newPoint; |
1319 | } else if (ts.isPressed) { |
1320 | otherPoint = true; |
1321 | points << makePoint(i, QEventPoint::Stationary, ts.position); |
1322 | } |
1323 | } |
1324 | |
1325 | QEvent::Type type; |
1326 | if (pointState == QEventPoint::Pressed && !otherPoint) |
1327 | type = QEvent::Type::TouchBegin; |
1328 | else if (pointState == QEventPoint::Released && !otherPoint) |
1329 | type = QEvent::Type::TouchEnd; |
1330 | else |
1331 | type = QEvent::Type::TouchUpdate; |
1332 | |
1333 | QTouchEvent ev(type, m_syntheticTouchDevice, {}, points); |
1334 | |
1335 | if (t) { |
1336 | // Actually send event: |
1337 | auto da = QQuickItemPrivate::get(item: t)->deliveryAgent(); |
1338 | bool handled = da->event(ev: &ev); |
1339 | Q_UNUSED(handled); |
1340 | } |
1341 | |
1342 | // Duplicate logic from QQuickWindowPrivate::clearGrabbers |
1343 | if (ev.isEndEvent()) { |
1344 | for (auto &point : ev.points()) { |
1345 | if (point.state() == QEventPoint::State::Released) { |
1346 | ev.setExclusiveGrabber(point, exclusiveGrabber: nullptr); |
1347 | ev.clearPassiveGrabbers(point); |
1348 | } |
1349 | } |
1350 | } |
1351 | }; |
1352 | |
1353 | // Send a release event to the previous target |
1354 | if (prevState.target && !sameTarget) |
1355 | sendTouchEvent(prevState.target, prevState.position, pointId, QEventPoint::Released); |
1356 | |
1357 | // Now send an event for the new state |
1358 | QEventPoint::State newState = isPress ? QEventPoint::Pressed : isRelease ? QEventPoint::Released : QEventPoint::Updated; |
1359 | sendTouchEvent(target, position, pointId, newState); |
1360 | } |
1361 | |
1362 | QQuick3DLightmapBaker *QQuick3DViewport::maybeLightmapBaker() |
1363 | { |
1364 | return m_lightmapBaker; |
1365 | } |
1366 | |
1367 | QQuick3DLightmapBaker *QQuick3DViewport::lightmapBaker() |
1368 | { |
1369 | if (!m_lightmapBaker) |
1370 | m_lightmapBaker= new QQuick3DLightmapBaker(this); |
1371 | |
1372 | return m_lightmapBaker; |
1373 | } |
1374 | |
1375 | /*! |
1376 | \internal |
1377 | */ |
1378 | void QQuick3DViewport::bakeLightmap() |
1379 | { |
1380 | lightmapBaker()->bake(); |
1381 | } |
1382 | |
1383 | void QQuick3DViewport::setGlobalPickingEnabled(bool isEnabled) |
1384 | { |
1385 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1386 | if (!renderer) |
1387 | return; |
1388 | |
1389 | renderer->setGlobalPickingEnabled(isEnabled); |
1390 | } |
1391 | |
1392 | void QQuick3DViewport::invalidateSceneGraph() |
1393 | { |
1394 | m_node = nullptr; |
1395 | } |
1396 | |
1397 | QQuick3DSceneRenderer *QQuick3DViewport::getRenderer() const |
1398 | { |
1399 | QQuick3DSceneRenderer *renderer = nullptr; |
1400 | if (m_node) { |
1401 | renderer = m_node->renderer; |
1402 | } else if (m_renderNode) { |
1403 | renderer = m_renderNode->renderer; |
1404 | } else if (m_directRenderer) { |
1405 | renderer = m_directRenderer->renderer(); |
1406 | } |
1407 | return renderer; |
1408 | } |
1409 | |
1410 | void QQuick3DViewport::updateDynamicTextures() |
1411 | { |
1412 | // Update QSGDynamicTextures that are used for source textures and Quick items |
1413 | // Must be called on the render thread. |
1414 | |
1415 | const auto &sceneManager = QQuick3DObjectPrivate::get(item: m_sceneRoot)->sceneManager; |
1416 | for (auto *texture : std::as_const(t&: sceneManager->qsgDynamicTextures)) |
1417 | texture->updateTexture(); |
1418 | |
1419 | QQuick3DNode *scene = m_importScene; |
1420 | while (scene) { |
1421 | const auto &importSm = QQuick3DObjectPrivate::get(item: scene)->sceneManager; |
1422 | if (importSm != sceneManager) { |
1423 | for (auto *texture : std::as_const(t&: importSm->qsgDynamicTextures)) |
1424 | texture->updateTexture(); |
1425 | } |
1426 | |
1427 | // if importScene has another import |
1428 | QQuick3DSceneRootNode *rn = qobject_cast<QQuick3DSceneRootNode *>(object: scene); |
1429 | scene = rn ? rn->view3D()->importScene() : nullptr; |
1430 | } |
1431 | } |
1432 | |
1433 | QSGNode *QQuick3DViewport::setupOffscreenRenderer(QSGNode *node) |
1434 | { |
1435 | SGFramebufferObjectNode *n = static_cast<SGFramebufferObjectNode *>(node); |
1436 | |
1437 | if (!n) { |
1438 | if (!m_node) |
1439 | m_node = new SGFramebufferObjectNode; |
1440 | n = m_node; |
1441 | } |
1442 | |
1443 | if (!n->renderer) { |
1444 | n->window = window(); |
1445 | n->renderer = createRenderer(); |
1446 | if (!n->renderer) |
1447 | return nullptr; |
1448 | n->renderer->fboNode = n; |
1449 | n->quickFbo = this; |
1450 | connect(sender: window(), SIGNAL(screenChanged(QScreen*)), receiver: n, SLOT(handleScreenChange())); |
1451 | } |
1452 | |
1453 | const qreal dpr = window()->effectiveDevicePixelRatio(); |
1454 | const QSize minFboSize = QQuickItemPrivate::get(item: this)->sceneGraphContext()->minimumFBOSize(); |
1455 | QSize desiredFboSize = QSize(m_explicitTextureWidth, m_explicitTextureHeight); |
1456 | if (desiredFboSize.isEmpty()) { |
1457 | desiredFboSize = QSize(width(), height()) * dpr; |
1458 | n->devicePixelRatio = dpr; |
1459 | // 1:1 mapping between the backing texture and the on-screen quad |
1460 | m_widthMultiplier = 1.0f; |
1461 | m_heightMultiplier = 1.0f; |
1462 | } else { |
1463 | QSize itemPixelSize = QSize(width(), height()) * dpr; |
1464 | // not 1:1 maping between the backing texture and the on-screen quad |
1465 | m_widthMultiplier = desiredFboSize.width() / float(itemPixelSize.width()); |
1466 | m_heightMultiplier = desiredFboSize.height() / float(itemPixelSize.height()); |
1467 | n->devicePixelRatio = 1.0; |
1468 | } |
1469 | desiredFboSize.setWidth(qMax(a: minFboSize.width(), b: desiredFboSize.width())); |
1470 | desiredFboSize.setHeight(qMax(a: minFboSize.height(), b: desiredFboSize.height())); |
1471 | |
1472 | if (desiredFboSize != m_effectiveTextureSize) { |
1473 | m_effectiveTextureSize = desiredFboSize; |
1474 | emit effectiveTextureSizeChanged(); |
1475 | } |
1476 | |
1477 | n->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest); |
1478 | n->setRect(x: 0, y: 0, w: width(), h: height()); |
1479 | if (checkIsVisible() && isComponentComplete()) { |
1480 | n->renderer->synchronize(view3D: this, size: desiredFboSize, dpr: n->devicePixelRatio); |
1481 | if (n->renderer->m_textureNeedsFlip) |
1482 | n->setTextureCoordinatesTransform(QSGSimpleTextureNode::MirrorVertically); |
1483 | updateDynamicTextures(); |
1484 | n->scheduleRender(); |
1485 | } |
1486 | |
1487 | return n; |
1488 | } |
1489 | |
1490 | QSGNode *QQuick3DViewport::setupInlineRenderer(QSGNode *node) |
1491 | { |
1492 | QQuick3DSGRenderNode *n = static_cast<QQuick3DSGRenderNode *>(node); |
1493 | if (!n) { |
1494 | if (!m_renderNode) |
1495 | m_renderNode = new QQuick3DSGRenderNode; |
1496 | n = m_renderNode; |
1497 | } |
1498 | |
1499 | if (!n->renderer) { |
1500 | n->window = window(); |
1501 | n->renderer = createRenderer(); |
1502 | if (!n->renderer) |
1503 | return nullptr; |
1504 | } |
1505 | |
1506 | if (!m_effectiveTextureSize.isEmpty()) { |
1507 | m_effectiveTextureSize = QSize(); |
1508 | emit effectiveTextureSizeChanged(); |
1509 | } |
1510 | |
1511 | const QSize targetSize = window()->effectiveDevicePixelRatio() * QSize(width(), height()); |
1512 | |
1513 | // checkIsVisible, not isVisible, because, for example, a |
1514 | // { visible: false; layer.enabled: true } item still needs |
1515 | // to function normally. |
1516 | if (checkIsVisible() && isComponentComplete()) { |
1517 | n->renderer->synchronize(view3D: this, size: targetSize, dpr: window()->effectiveDevicePixelRatio()); |
1518 | updateDynamicTextures(); |
1519 | n->markDirty(bits: QSGNode::DirtyMaterial); |
1520 | } |
1521 | |
1522 | return n; |
1523 | } |
1524 | |
1525 | |
1526 | void QQuick3DViewport::setupDirectRenderer(RenderMode mode) |
1527 | { |
1528 | auto renderMode = (mode == Underlay) ? QQuick3DSGDirectRenderer::Underlay |
1529 | : QQuick3DSGDirectRenderer::Overlay; |
1530 | if (!m_directRenderer) { |
1531 | QQuick3DSceneRenderer *sceneRenderer = createRenderer(); |
1532 | if (!sceneRenderer) |
1533 | return; |
1534 | m_directRenderer = new QQuick3DSGDirectRenderer(sceneRenderer, window(), renderMode); |
1535 | connect(sender: window(), signal: &QQuickWindow::sceneGraphInvalidated, context: this, slot: &QQuick3DViewport::cleanupDirectRenderer, type: Qt::DirectConnection); |
1536 | } |
1537 | |
1538 | if (!m_effectiveTextureSize.isEmpty()) { |
1539 | m_effectiveTextureSize = QSize(); |
1540 | emit effectiveTextureSizeChanged(); |
1541 | } |
1542 | |
1543 | const QSizeF targetSize = window()->effectiveDevicePixelRatio() * QSizeF(width(), height()); |
1544 | m_directRenderer->setViewport(QRectF(window()->effectiveDevicePixelRatio() * mapToScene(point: QPointF(0, 0)), targetSize)); |
1545 | m_directRenderer->setVisibility(isVisible()); |
1546 | if (isVisible()) { |
1547 | m_directRenderer->preSynchronize(); |
1548 | m_directRenderer->renderer()->synchronize(view3D: this, size: targetSize.toSize(), dpr: window()->effectiveDevicePixelRatio()); |
1549 | updateDynamicTextures(); |
1550 | m_directRenderer->requestRender(); |
1551 | } |
1552 | } |
1553 | |
1554 | // This is used for offscreen mode since we need to check if |
1555 | // this item is used by an effect but hidden |
1556 | bool QQuick3DViewport::checkIsVisible() const |
1557 | { |
1558 | auto childPrivate = QQuickItemPrivate::get(item: this); |
1559 | return (childPrivate->explicitVisible || |
1560 | (childPrivate->extra.isAllocated() && childPrivate->extra->effectRefCount)); |
1561 | |
1562 | } |
1563 | |
1564 | /*! |
1565 | \internal |
1566 | |
1567 | This method processes a pickResult on an object with the objective of checking |
1568 | if there are any QQuickItem based subscenes that the QPointerEvent needs to be |
1569 | forwarded to (3D -> 2D). If such a subscene is found, the event will be mapped |
1570 | to the correct cordinate system, and added to the visitedSubscenes map for later |
1571 | event delivery. |
1572 | */ |
1573 | void QQuick3DViewport::processPickedObject(const QSSGRenderPickResult &pickResult, |
1574 | int pointIndex, |
1575 | QPointerEvent *event, |
1576 | QFlatMap<QQuickItem *, SubsceneInfo> &visitedSubscenes) const |
1577 | { |
1578 | QQuickItem *subsceneRootItem = nullptr; |
1579 | QPointF subscenePosition; |
1580 | const auto backendObject = pickResult.m_hitObject; |
1581 | const auto frontendObject = findFrontendNode(backendObject); |
1582 | if (!frontendObject) |
1583 | return; |
1584 | |
1585 | // Figure out if there are any QQuickItem based scenes we need to forward |
1586 | // the event to, and if the are found, determine how to translate the UV Coords |
1587 | // in the pickResult based on what type the object containing the scene is. |
1588 | auto frontendObjectPrivate = QQuick3DObjectPrivate::get(item: frontendObject); |
1589 | if (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Item2D) { |
1590 | // Item2D, this is the case where there is just an embedded Qt Quick 2D Item |
1591 | // rendered directly to the scene. |
1592 | auto item2D = qobject_cast<QQuick3DItem2D *>(object: frontendObject); |
1593 | if (item2D) |
1594 | subsceneRootItem = item2D->contentItem(); |
1595 | if (!subsceneRootItem || subsceneRootItem->childItems().isEmpty()) |
1596 | return; // ignore empty 2D subscenes |
1597 | |
1598 | // In this case the "UV" coordinates are in pixels in the subscene root item's coordinate system. |
1599 | subscenePosition = pickResult.m_localUVCoords.toPointF(); |
1600 | |
1601 | // The following code will account for custom input masking, as well any |
1602 | // transformations that might have been applied to the Item |
1603 | if (!subsceneRootItem->childAt(x: subscenePosition.x(), y: subscenePosition.y())) |
1604 | return; |
1605 | } else if (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Model) { |
1606 | // Model |
1607 | int materialSubset = pickResult.m_subset; |
1608 | const auto backendModel = static_cast<const QSSGRenderModel *>(backendObject); |
1609 | // Get material |
1610 | if (backendModel->materials.size() < (pickResult.m_subset + 1)) |
1611 | materialSubset = backendModel->materials.size() - 1; |
1612 | if (materialSubset < 0) |
1613 | return; |
1614 | const auto backendMaterial = backendModel->materials.at(i: materialSubset); |
1615 | const auto frontendMaterial = static_cast<QQuick3DMaterial*>(findFrontendNode(backendObject: backendMaterial)); |
1616 | subsceneRootItem = getSubSceneRootItem(material: frontendMaterial); |
1617 | |
1618 | if (subsceneRootItem) { |
1619 | // In this case the pick result really is using UV coordinates. |
1620 | subscenePosition = QPointF(subsceneRootItem->x() + pickResult.m_localUVCoords.x() * subsceneRootItem->width(), |
1621 | subsceneRootItem->y() - pickResult.m_localUVCoords.y() * subsceneRootItem->height() + subsceneRootItem->height()); |
1622 | } |
1623 | } |
1624 | |
1625 | // Add the new event (item and position) to the visitedSubscene map. |
1626 | if (subsceneRootItem) { |
1627 | SubsceneInfo &subscene = visitedSubscenes[subsceneRootItem]; // create if not found |
1628 | subscene.obj = frontendObject; |
1629 | if (subscene.eventPointScenePositions.size() != event->pointCount()) { |
1630 | // ensure capacity, and use an out-of-scene position rather than 0,0 by default |
1631 | constexpr QPointF inf(-qt_inf(), -qt_inf()); |
1632 | subscene.eventPointScenePositions.resize(sz: event->pointCount(), v: inf); |
1633 | } |
1634 | subscene.eventPointScenePositions[pointIndex] = subscenePosition; |
1635 | } |
1636 | } |
1637 | |
1638 | /*! |
1639 | \internal |
1640 | |
1641 | This method will try and find a QQuickItem based by looking for sourceItem() |
1642 | properties on the primary color channels for the various material types. |
1643 | |
1644 | \note for CustomMaterial there is just a best effort given where the first |
1645 | associated Texture with a sourceItem property is used. |
1646 | */ |
1647 | |
1648 | QQuickItem *QQuick3DViewport::getSubSceneRootItem(QQuick3DMaterial *material) const |
1649 | { |
1650 | if (!material) |
1651 | return nullptr; |
1652 | |
1653 | QQuickItem *subsceneRootItem = nullptr; |
1654 | const auto frontendMaterialPrivate = QQuick3DObjectPrivate::get(item: material); |
1655 | |
1656 | if (frontendMaterialPrivate->type == QQuick3DObjectPrivate::Type::DefaultMaterial) { |
1657 | // Default Material |
1658 | const auto defaultMaterial = qobject_cast<QQuick3DDefaultMaterial *>(object: material); |
1659 | if (defaultMaterial) { |
1660 | // Just check for a diffuseMap for now |
1661 | if (defaultMaterial->diffuseMap() && defaultMaterial->diffuseMap()->sourceItem()) |
1662 | subsceneRootItem = defaultMaterial->diffuseMap()->sourceItem(); |
1663 | } |
1664 | |
1665 | } else if (frontendMaterialPrivate->type == QQuick3DObjectPrivate::Type::PrincipledMaterial) { |
1666 | // Principled Material |
1667 | const auto principledMaterial = qobject_cast<QQuick3DPrincipledMaterial *>(object: material); |
1668 | if (principledMaterial) { |
1669 | // Just check for a baseColorMap for now |
1670 | if (principledMaterial->baseColorMap() && principledMaterial->baseColorMap()->sourceItem()) |
1671 | subsceneRootItem = principledMaterial->baseColorMap()->sourceItem(); |
1672 | } |
1673 | } else if (frontendMaterialPrivate->type == QQuick3DObjectPrivate::Type::SpecularGlossyMaterial) { |
1674 | // SpecularGlossy Material |
1675 | const auto specularGlossyMaterial = qobject_cast<QQuick3DSpecularGlossyMaterial *>(object: material); |
1676 | if (specularGlossyMaterial) { |
1677 | // Just check for a albedoMap for now |
1678 | if (specularGlossyMaterial->albedoMap() && specularGlossyMaterial->albedoMap()->sourceItem()) |
1679 | subsceneRootItem = specularGlossyMaterial->albedoMap()->sourceItem(); |
1680 | } |
1681 | } else if (frontendMaterialPrivate->type == QQuick3DObjectPrivate::Type::CustomMaterial) { |
1682 | // Custom Material |
1683 | const auto customMaterial = qobject_cast<QQuick3DCustomMaterial *>(object: material); |
1684 | if (customMaterial) { |
1685 | // This case is a bit harder because we can not know how the textures will be used |
1686 | const auto &texturesInputs = customMaterial->m_dynamicTextureMaps; |
1687 | for (const auto &textureInput : texturesInputs) { |
1688 | if (auto texture = textureInput->texture()) { |
1689 | if (texture->sourceItem()) { |
1690 | subsceneRootItem = texture->sourceItem(); |
1691 | break; |
1692 | } |
1693 | } |
1694 | } |
1695 | } |
1696 | } |
1697 | return subsceneRootItem; |
1698 | } |
1699 | |
1700 | |
1701 | /*! |
1702 | \internal |
1703 | */ |
1704 | QQuick3DPickResult QQuick3DViewport::getNearestPickResult(const QVarLengthArray<QSSGRenderPickResult, 20> &pickResults) const |
1705 | { |
1706 | for (const auto &result : pickResults) { |
1707 | auto pickResult = processPickResult(pickResult: result); |
1708 | if (pickResult.hitType() != QQuick3DPickResultEnums::HitType::Null) |
1709 | return pickResult; |
1710 | } |
1711 | |
1712 | return QQuick3DPickResult(); |
1713 | } |
1714 | |
1715 | /*! |
1716 | \internal |
1717 | This method is responsible for going through the visitedSubscenes map and forwarding |
1718 | the event with corrected coordinates to each subscene. |
1719 | |
1720 | */ |
1721 | bool QQuick3DViewport::forwardEventToSubscenes(QPointerEvent *event, |
1722 | bool useRayPicking, |
1723 | QQuick3DSceneRenderer *renderer, |
1724 | const QFlatMap<QQuickItem *, SubsceneInfo> &visitedSubscenes) const |
1725 | { |
1726 | // Now deliver the entire event (all points) to each relevant subscene. |
1727 | // Maybe only some points fall inside, but QQuickDeliveryAgentPrivate::deliverMatchingPointsToItem() |
1728 | // makes reduced-subset touch events that contain only the relevant points, when necessary. |
1729 | bool ret = false; |
1730 | |
1731 | QVarLengthArray<QPointF, 16> originalScenePositions; |
1732 | originalScenePositions.resize(sz: event->pointCount()); |
1733 | for (int pointIndex = 0; pointIndex < event->pointCount(); ++pointIndex) |
1734 | originalScenePositions[pointIndex] = event->point(i: pointIndex).scenePosition(); |
1735 | for (auto subscene : visitedSubscenes) { |
1736 | QQuickItem *subsceneRoot = subscene.first; |
1737 | auto &subsceneInfo = subscene.second; |
1738 | Q_ASSERT(subsceneInfo.eventPointScenePositions.size() == event->pointCount()); |
1739 | auto da = QQuickItemPrivate::get(item: subsceneRoot)->deliveryAgent(); |
1740 | for (int pointIndex = 0; pointIndex < event->pointCount(); ++pointIndex) { |
1741 | const auto &pt = subsceneInfo.eventPointScenePositions.at(idx: pointIndex); |
1742 | // By tradition, QGuiApplicationPrivate::processTouchEvent() has set the local position to the scene position, |
1743 | // and Qt Quick expects it to arrive that way: then QQuickDeliveryAgentPrivate::translateTouchEvent() |
1744 | // copies it into the scene position before localizing. |
1745 | // That might be silly, we might change it eventually, but gotta stay consistent for now. |
1746 | QEventPoint &ep = event->point(i: pointIndex); |
1747 | QMutableEventPoint::setPosition(p&: ep, arg: pt); |
1748 | QMutableEventPoint::setScenePosition(p&: ep, arg: pt); |
1749 | } |
1750 | |
1751 | if (event->isBeginEvent()) |
1752 | da->setSceneTransform(nullptr); |
1753 | if (da->event(ev: event)) { |
1754 | ret = true; |
1755 | if (QQuickDeliveryAgentPrivate::anyPointGrabbed(ev: event) && !useRayPicking) { |
1756 | // In case any QEventPoint was grabbed, the relevant QQuickDeliveryAgent needs to know |
1757 | // how to repeat the picking/coordinate transformation for each update, |
1758 | // because delivery will bypass internalPick() due to the grab, and it's |
1759 | // more efficient to avoid whole-scene picking each time anyway. |
1760 | auto frontendObjectPrivate = QQuick3DObjectPrivate::get(item: subsceneInfo.obj); |
1761 | const bool item2Dcase = (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Item2D); |
1762 | ViewportTransformHelper *transform = new ViewportTransformHelper; |
1763 | transform->viewport = const_cast<QQuick3DViewport *>(this); |
1764 | transform->renderer = renderer; |
1765 | transform->sceneParentNode = static_cast<QSSGRenderNode*>(frontendObjectPrivate->spatialNode); |
1766 | transform->targetItem = subsceneRoot; |
1767 | transform->scaleX = window()->effectiveDevicePixelRatio() * m_widthMultiplier; |
1768 | transform->scaleY = window()->effectiveDevicePixelRatio() * m_heightMultiplier; |
1769 | transform->uvCoordsArePixels = item2Dcase; |
1770 | transform->setOnDeliveryAgent(da); |
1771 | qCDebug(lcPick) << event->type() << "created ViewportTransformHelper on" << da; |
1772 | } |
1773 | } else if (event->type() != QEvent::HoverMove) { |
1774 | qCDebug(lcPick) << subsceneRoot << "didn't want" << event; |
1775 | } |
1776 | event->setAccepted(false); // reject implicit grab and let it keep propagating |
1777 | } |
1778 | if (visitedSubscenes.isEmpty()) { |
1779 | event->setAccepted(false); |
1780 | } else { |
1781 | for (int pointIndex = 0; pointIndex < event->pointCount(); ++pointIndex) |
1782 | QMutableEventPoint::setScenePosition(p&: event->point(i: pointIndex), arg: originalScenePositions.at(idx: pointIndex)); |
1783 | } |
1784 | |
1785 | // Normally this would occur in QQuickWindowPrivate::clearGrabbers(...) but |
1786 | // for ray based input, input never goes through QQuickWindow (since events |
1787 | // are generated from within scene space and not window/screen space). |
1788 | if (event->isEndEvent() && useRayPicking) { |
1789 | if (event->isSinglePointEvent()) { |
1790 | if (static_cast<QSinglePointEvent *>(event)->buttons() == Qt::NoButton) { |
1791 | auto &firstPt = event->point(i: 0); |
1792 | event->setExclusiveGrabber(point: firstPt, exclusiveGrabber: nullptr); |
1793 | event->clearPassiveGrabbers(point: firstPt); |
1794 | } |
1795 | } else { |
1796 | for (auto &point : event->points()) { |
1797 | if (point.state() == QEventPoint::State::Released) { |
1798 | event->setExclusiveGrabber(point, exclusiveGrabber: nullptr); |
1799 | event->clearPassiveGrabbers(point); |
1800 | } |
1801 | } |
1802 | } |
1803 | } |
1804 | |
1805 | return ret; |
1806 | } |
1807 | |
1808 | |
1809 | bool QQuick3DViewport::internalPick(QPointerEvent *event, const QVector3D &origin, const QVector3D &direction) const |
1810 | { |
1811 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1812 | if (!renderer || !event) |
1813 | return false; |
1814 | |
1815 | QFlatMap<QQuickItem*, SubsceneInfo> visitedSubscenes; |
1816 | const bool useRayPicking = !direction.isNull(); |
1817 | |
1818 | for (int pointIndex = 0; pointIndex < event->pointCount(); ++pointIndex) { |
1819 | auto &eventPoint = event->point(i: pointIndex); |
1820 | QVarLengthArray<QSSGRenderPickResult, 20> pickResults; |
1821 | if (Q_UNLIKELY(useRayPicking)) |
1822 | pickResults = getPickResults(renderer, origin, direction); |
1823 | else |
1824 | pickResults = getPickResults(renderer, eventPoint); |
1825 | |
1826 | if (!pickResults.isEmpty()) |
1827 | for (const auto &pickResult : pickResults) |
1828 | processPickedObject(pickResult, pointIndex, event, visitedSubscenes); |
1829 | else |
1830 | eventPoint.setAccepted(false); // let it fall through the viewport to Items underneath |
1831 | } |
1832 | |
1833 | return forwardEventToSubscenes(event, useRayPicking, renderer, visitedSubscenes); |
1834 | } |
1835 | |
1836 | bool QQuick3DViewport::singlePointPick(QSinglePointEvent *event, const QVector3D &origin, const QVector3D &direction) |
1837 | { |
1838 | QQuick3DSceneRenderer *renderer = getRenderer(); |
1839 | if (!renderer || !event) |
1840 | return false; |
1841 | |
1842 | QSSGRenderRay ray(origin, direction); |
1843 | |
1844 | Q_ASSERT(event->pointCount() == 1); |
1845 | QPointF originalPosition = event->point(i: 0).scenePosition(); |
1846 | |
1847 | auto pickResults = renderer->syncPickAll(ray); |
1848 | |
1849 | bool delivered = false; |
1850 | |
1851 | constexpr float jitterLimit = 2.5; // TODO: add property for this? |
1852 | bool withinJitterLimit = false; |
1853 | |
1854 | for (const auto &pickResult : pickResults) { |
1855 | auto [item, position] = getItemAndPosition(pickResult); |
1856 | if (!item) |
1857 | continue; |
1858 | if (item == m_prevMouseItem && (position - m_prevMousePos).manhattanLength() < jitterLimit && !event->button()) { |
1859 | withinJitterLimit = true; |
1860 | break; |
1861 | } |
1862 | auto da = QQuickItemPrivate::get(item)->deliveryAgent(); |
1863 | QEventPoint &ep = event->point(i: 0); |
1864 | QMutableEventPoint::setPosition(p&: ep, arg: position); |
1865 | QMutableEventPoint::setScenePosition(p&: ep, arg: position); |
1866 | if (da->event(ev: event)) { |
1867 | delivered = true; |
1868 | if (event->type() == QEvent::MouseButtonPress) { |
1869 | m_prevMouseItem = item; |
1870 | m_prevMousePos = position; |
1871 | withinJitterLimit = true; |
1872 | } |
1873 | break; |
1874 | } |
1875 | } |
1876 | |
1877 | QMutableEventPoint::setScenePosition(p&: event->point(i: 0), arg: originalPosition); |
1878 | if (!withinJitterLimit) |
1879 | m_prevMouseItem = nullptr; |
1880 | |
1881 | // Normally this would occur in QQuickWindowPrivate::clearGrabbers(...) but |
1882 | // for ray based input, input never goes through QQuickWindow (since events |
1883 | // are generated from within scene space and not window/screen space). |
1884 | if (event->isEndEvent()) { |
1885 | if (event->buttons() == Qt::NoButton) { |
1886 | auto &firstPt = event->point(i: 0); |
1887 | event->setExclusiveGrabber(point: firstPt, exclusiveGrabber: nullptr); |
1888 | event->clearPassiveGrabbers(point: firstPt); |
1889 | } |
1890 | } |
1891 | |
1892 | return delivered; |
1893 | } |
1894 | |
1895 | QPair<QQuickItem *, QPointF> QQuick3DViewport::getItemAndPosition(const QSSGRenderPickResult &pickResult) |
1896 | { |
1897 | QQuickItem *subsceneRootItem = nullptr; |
1898 | QPointF subscenePosition; |
1899 | const auto backendObject = pickResult.m_hitObject; |
1900 | const auto frontendObject = findFrontendNode(backendObject); |
1901 | if (!frontendObject) |
1902 | return {}; |
1903 | auto frontendObjectPrivate = QQuick3DObjectPrivate::get(item: frontendObject); |
1904 | if (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Item2D) { |
1905 | // Item2D, this is the case where there is just an embedded Qt Quick 2D Item |
1906 | // rendered directly to the scene. |
1907 | auto item2D = qobject_cast<QQuick3DItem2D *>(object: frontendObject); |
1908 | if (item2D) |
1909 | subsceneRootItem = item2D->contentItem(); |
1910 | if (!subsceneRootItem || subsceneRootItem->childItems().isEmpty()) |
1911 | return {}; // ignore empty 2D subscenes |
1912 | |
1913 | // In this case the "UV" coordinates are in pixels in the subscene root item's coordinate system. |
1914 | subscenePosition = pickResult.m_localUVCoords.toPointF(); |
1915 | |
1916 | // The following code will account for custom input masking, as well any |
1917 | // transformations that might have been applied to the Item |
1918 | if (!subsceneRootItem->childAt(x: subscenePosition.x(), y: subscenePosition.y())) |
1919 | return {}; |
1920 | } else if (frontendObjectPrivate->type == QQuick3DObjectPrivate::Type::Model) { |
1921 | // Model |
1922 | int materialSubset = pickResult.m_subset; |
1923 | const auto backendModel = static_cast<const QSSGRenderModel *>(backendObject); |
1924 | // Get material |
1925 | if (backendModel->materials.size() < (pickResult.m_subset + 1)) |
1926 | materialSubset = backendModel->materials.size() - 1; |
1927 | if (materialSubset < 0) |
1928 | return {}; |
1929 | const auto backendMaterial = backendModel->materials.at(i: materialSubset); |
1930 | const auto frontendMaterial = static_cast<QQuick3DMaterial *>(findFrontendNode(backendObject: backendMaterial)); |
1931 | subsceneRootItem = getSubSceneRootItem(material: frontendMaterial); |
1932 | |
1933 | if (subsceneRootItem) { |
1934 | // In this case the pick result really is using UV coordinates. |
1935 | subscenePosition = QPointF(subsceneRootItem->x() + pickResult.m_localUVCoords.x() * subsceneRootItem->width(), |
1936 | subsceneRootItem->y() - pickResult.m_localUVCoords.y() * subsceneRootItem->height() + subsceneRootItem->height()); |
1937 | } |
1938 | } |
1939 | return {subsceneRootItem, subscenePosition}; |
1940 | } |
1941 | |
1942 | QVarLengthArray<QSSGRenderPickResult, 20> QQuick3DViewport::getPickResults(QQuick3DSceneRenderer *renderer, |
1943 | const QVector3D &origin, |
1944 | const QVector3D &direction) const |
1945 | { |
1946 | const QSSGRenderRay ray(origin, direction); |
1947 | return renderer->syncPickAll(ray); |
1948 | } |
1949 | |
1950 | QVarLengthArray<QSSGRenderPickResult, 20> QQuick3DViewport::getPickResults(QQuick3DSceneRenderer *renderer, const QEventPoint &eventPoint) const |
1951 | { |
1952 | QVarLengthArray<QSSGRenderPickResult, 20> pickResults; |
1953 | QPointF realPosition = eventPoint.position() * window()->effectiveDevicePixelRatio(); |
1954 | // correct when mapping is not 1:1 due to explicit backing texture size |
1955 | realPosition.rx() *= m_widthMultiplier; |
1956 | realPosition.ry() *= m_heightMultiplier; |
1957 | std::optional<QSSGRenderRay> rayResult = renderer->getRayFromViewportPos(pos: realPosition); |
1958 | if (rayResult.has_value()) |
1959 | pickResults = renderer->syncPickAll(ray: rayResult.value()); |
1960 | return pickResults; |
1961 | } |
1962 | |
1963 | /*! |
1964 | \internal |
1965 | |
1966 | This provides a way to lookup frontendNodes with a backend node taking into consideration both |
1967 | the scene and the importScene |
1968 | */ |
1969 | QQuick3DObject *QQuick3DViewport::findFrontendNode(const QSSGRenderGraphObject *backendObject) const |
1970 | { |
1971 | if (!backendObject) |
1972 | return nullptr; |
1973 | |
1974 | const auto sceneManager = QQuick3DObjectPrivate::get(item: m_sceneRoot)->sceneManager; |
1975 | QQuick3DObject *frontendObject = sceneManager->lookUpNode(node: backendObject); |
1976 | if (!frontendObject && m_importScene) { |
1977 | const auto importSceneManager = QQuick3DObjectPrivate::get(item: m_importScene)->sceneManager; |
1978 | frontendObject = importSceneManager->lookUpNode(node: backendObject); |
1979 | } |
1980 | return frontendObject; |
1981 | } |
1982 | |
1983 | QQuick3DPickResult QQuick3DViewport::processPickResult(const QSSGRenderPickResult &pickResult) const |
1984 | { |
1985 | if (!pickResult.m_hitObject) |
1986 | return QQuick3DPickResult(); |
1987 | |
1988 | QQuick3DObject *frontendObject = findFrontendNode(backendObject: pickResult.m_hitObject); |
1989 | |
1990 | QQuick3DModel *model = qobject_cast<QQuick3DModel *>(object: frontendObject); |
1991 | if (model) |
1992 | return QQuick3DPickResult(model, |
1993 | ::sqrtf(x: pickResult.m_distanceSq), |
1994 | pickResult.m_localUVCoords, |
1995 | pickResult.m_scenePosition, |
1996 | pickResult.m_localPosition, |
1997 | pickResult.m_faceNormal, |
1998 | pickResult.m_instanceIndex); |
1999 | |
2000 | QQuick3DItem2D *frontend2DItem = qobject_cast<QQuick3DItem2D *>(object: frontendObject); |
2001 | if (frontend2DItem && frontend2DItem->contentItem()) { |
2002 | // Check if the pick is inside the content item (since the ray just intersected on the items plane) |
2003 | const QPointF subscenePosition = pickResult.m_localUVCoords.toPointF(); |
2004 | const auto child = frontend2DItem->contentItem()->childAt(x: subscenePosition.x(), y: subscenePosition.y()); |
2005 | if (child) { |
2006 | return QQuick3DPickResult(child, |
2007 | ::sqrtf(x: pickResult.m_distanceSq), |
2008 | QVector2D(frontend2DItem->contentItem()->mapToItem(item: child, point: subscenePosition)), |
2009 | pickResult.m_scenePosition, |
2010 | pickResult.m_localPosition, |
2011 | pickResult.m_faceNormal); |
2012 | } |
2013 | } |
2014 | |
2015 | return QQuick3DPickResult(); |
2016 | |
2017 | } |
2018 | |
2019 | // Returns the first found scene manager of objects children |
2020 | QQuick3DSceneManager *QQuick3DViewport::findChildSceneManager(QQuick3DObject *inObject, QQuick3DSceneManager *manager) |
2021 | { |
2022 | if (manager) |
2023 | return manager; |
2024 | |
2025 | auto children = QQuick3DObjectPrivate::get(item: inObject)->childItems; |
2026 | for (auto child : children) { |
2027 | if (auto m = QQuick3DObjectPrivate::get(item: child)->sceneManager) { |
2028 | manager = m; |
2029 | break; |
2030 | } |
2031 | manager = findChildSceneManager(inObject: child, manager); |
2032 | } |
2033 | return manager; |
2034 | } |
2035 | |
2036 | void QQuick3DViewport::updateInputProcessing() |
2037 | { |
2038 | // This should be called from the gui thread. |
2039 | setAcceptTouchEvents(m_enableInputProcessing); |
2040 | setAcceptHoverEvents(m_enableInputProcessing); |
2041 | setAcceptedMouseButtons(m_enableInputProcessing ? Qt::AllButtons : Qt::NoButton); |
2042 | } |
2043 | |
2044 | void QQuick3DViewport::onReleaseCachedResources() |
2045 | { |
2046 | if (auto renderer = getRenderer()) |
2047 | renderer->releaseCachedResources(); |
2048 | } |
2049 | |
2050 | /*! |
2051 | \qmlproperty List<QtQuick3D::Object3D> View3D::extensions |
2052 | |
2053 | This property contains a list of user extensions that should be used with this \l View3D. |
2054 | |
2055 | \sa RenderExtension |
2056 | */ |
2057 | QQmlListProperty<QQuick3DObject> QQuick3DViewport::extensions() |
2058 | { |
2059 | return QQmlListProperty<QQuick3DObject>{ this, |
2060 | &m_extensionListDirty, |
2061 | &QQuick3DExtensionListHelper::extensionAppend, |
2062 | &QQuick3DExtensionListHelper::extensionCount, |
2063 | &QQuick3DExtensionListHelper::extensionAt, |
2064 | &QQuick3DExtensionListHelper::extensionClear, |
2065 | &QQuick3DExtensionListHelper::extensionReplace, |
2066 | &QQuick3DExtensionListHelper::extensionRemoveLast}; |
2067 | } |
2068 | |
2069 | /*! |
2070 | \internal |
2071 | */ |
2072 | void QQuick3DViewport::rebuildExtensionList() |
2073 | { |
2074 | m_extensionListDirty = true; |
2075 | update(); |
2076 | } |
2077 | |
2078 | QT_END_NAMESPACE |
2079 | |