1/****************************************************************************
2**
3** Copyright (C) 2015 Paul Lemire paul.lemire350@gmail.com
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt3D module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "pickboundingvolumejob_p.h"
41#include "qpicktriangleevent.h"
42#include "qpicklineevent.h"
43#include "qpickpointevent.h"
44#include <Qt3DCore/private/qaspectmanager_p.h>
45#include <Qt3DRender/qobjectpicker.h>
46#include <Qt3DRender/qviewport.h>
47#include <Qt3DRender/qgeometryrenderer.h>
48#include <Qt3DRender/private/qobjectpicker_p.h>
49#include <Qt3DRender/private/nodemanagers_p.h>
50#include <Qt3DRender/private/entity_p.h>
51#include <Qt3DRender/private/objectpicker_p.h>
52#include <Qt3DRender/private/managers_p.h>
53#include <Qt3DRender/private/geometryrenderer_p.h>
54#include <Qt3DRender/private/rendersettings_p.h>
55#include <Qt3DRender/private/trianglesvisitor_p.h>
56#include <Qt3DRender/private/job_common_p.h>
57#include <Qt3DRender/private/qpickevent_p.h>
58#include <Qt3DRender/private/pickboundingvolumeutils_p.h>
59
60#include <QSurface>
61#include <QWindow>
62#include <QOffscreenSurface>
63
64QT_BEGIN_NAMESPACE
65
66namespace Qt3DRender {
67using namespace Qt3DRender::RayCasting;
68
69namespace Render {
70
71class PickBoundingVolumeJobPrivate : public Qt3DCore::QAspectJobPrivate
72{
73public:
74 PickBoundingVolumeJobPrivate(PickBoundingVolumeJob *q) : q_ptr(q) { }
75 ~PickBoundingVolumeJobPrivate() override = default;
76
77 bool isRequired() const override;
78 void postFrame(Qt3DCore::QAspectManager *manager) override;
79
80 enum CustomEventType {
81 MouseButtonClick = QEvent::User,
82 };
83
84 struct EventDetails {
85 Qt3DCore::QNodeId pickerId;
86 int sourceEventType;
87 QPickEventPtr resultingEvent;
88 Qt3DCore::QNodeId viewportNodeId;
89 };
90
91 QVector<EventDetails> dispatches;
92 PickBoundingVolumeJob *q_ptr;
93 Q_DECLARE_PUBLIC(PickBoundingVolumeJob)
94};
95
96
97bool PickBoundingVolumeJobPrivate::isRequired() const
98{
99 Q_Q(const PickBoundingVolumeJob);
100 return !q->m_pendingMouseEvents.isEmpty() || q->m_pickersDirty || q->m_oneEnabledAtLeast;
101}
102
103void PickBoundingVolumeJobPrivate::postFrame(Qt3DCore::QAspectManager *manager)
104{
105 using namespace Qt3DCore;
106 QNodeId previousId;
107 QObjectPicker *node = nullptr;
108
109 for (auto res: qAsConst(t&: dispatches)) {
110 if (previousId != res.pickerId) {
111 node = qobject_cast<QObjectPicker *>(object: manager->lookupNode(id: res.pickerId));
112 previousId = res.pickerId;
113 }
114 if (!node)
115 continue;
116
117 QObjectPickerPrivate *dnode = static_cast<QObjectPickerPrivate *>(QObjectPickerPrivate::get(q: node));
118
119 // resolve front end details
120 QPickEvent *pickEvent = res.resultingEvent.data();
121 if (pickEvent) {
122 QPickEventPrivate *dpickEvent = QPickEventPrivate::get(object: pickEvent);
123 dpickEvent->m_viewport = static_cast<QViewport *>(manager->lookupNode(id: res.viewportNodeId));
124 dpickEvent->m_entityPtr = static_cast<QEntity *>(manager->lookupNode(id: dpickEvent->m_entity));
125 }
126
127 // dispatch event
128 switch (res.sourceEventType) {
129 case QEvent::MouseButtonPress:
130 dnode->pressedEvent(event: pickEvent);
131 break;
132 case QEvent::MouseButtonRelease:
133 dnode->releasedEvent(event: pickEvent);
134 break;
135 case MouseButtonClick:
136 dnode->clickedEvent(event: pickEvent);
137 break;
138 case QEvent::MouseMove:
139 dnode->movedEvent(event: pickEvent);
140 break;
141 case QEvent::Enter:
142 emit node->entered();
143 dnode->setContainsMouse(true);
144 break;
145 case QEvent::Leave:
146 dnode->setContainsMouse(false);
147 emit node->exited();
148 break;
149 default: Q_UNREACHABLE();
150 }
151 }
152
153 dispatches.clear();
154}
155
156namespace {
157
158void setEventButtonAndModifiers(const QMouseEvent &event, QPickEvent::Buttons &eventButton, int &eventButtons, int &eventModifiers)
159{
160 switch (event.button()) {
161 case Qt::LeftButton:
162 eventButton = QPickEvent::LeftButton;
163 break;
164 case Qt::RightButton:
165 eventButton = QPickEvent::RightButton;
166 break;
167 case Qt::MiddleButton:
168 eventButton = QPickEvent::MiddleButton;
169 break;
170 case Qt::BackButton:
171 eventButton = QPickEvent::BackButton;
172 break;
173 default:
174 break;
175 }
176
177 if (event.buttons() & Qt::LeftButton)
178 eventButtons |= QPickEvent::LeftButton;
179 if (event.buttons() & Qt::RightButton)
180 eventButtons |= QPickEvent::RightButton;
181 if (event.buttons() & Qt::MiddleButton)
182 eventButtons |= QPickEvent::MiddleButton;
183 if (event.buttons() & Qt::BackButton)
184 eventButtons |= QPickEvent::BackButton;
185 if (event.modifiers() & Qt::ShiftModifier)
186 eventModifiers |= QPickEvent::ShiftModifier;
187 if (event.modifiers() & Qt::ControlModifier)
188 eventModifiers |= QPickEvent::ControlModifier;
189 if (event.modifiers() & Qt::AltModifier)
190 eventModifiers |= QPickEvent::AltModifier;
191 if (event.modifiers() & Qt::MetaModifier)
192 eventModifiers |= QPickEvent::MetaModifier;
193 if (event.modifiers() & Qt::KeypadModifier)
194 eventModifiers |= QPickEvent::KeypadModifier;
195}
196
197} // anonymous
198
199PickBoundingVolumeJob::PickBoundingVolumeJob()
200 : AbstractPickingJob(*new PickBoundingVolumeJobPrivate(this))
201 , m_pickersDirty(true)
202{
203 SET_JOB_RUN_STAT_TYPE(this, JobTypes::PickBoundingVolume, 0)
204}
205
206void PickBoundingVolumeJob::setRoot(Entity *root)
207{
208 m_node = root;
209}
210
211void PickBoundingVolumeJob::setMouseEvents(const QList<QPair<QObject*, QMouseEvent>> &pendingEvents)
212{
213 m_pendingMouseEvents.append(t: pendingEvents);
214}
215
216void PickBoundingVolumeJob::setKeyEvents(const QList<QKeyEvent> &pendingEvents)
217{
218 m_pendingKeyEvents.append(t: pendingEvents);
219}
220
221void PickBoundingVolumeJob::markPickersDirty()
222{
223 m_pickersDirty = true;
224}
225
226bool PickBoundingVolumeJob::runHelper()
227{
228 // Move to clear the events so that we don't process them several times
229 // if run is called several times
230 const auto mouseEvents = std::move(m_pendingMouseEvents);
231
232 // If we have no events return early
233 if (mouseEvents.empty())
234 return false;
235
236 // Quickly look which picker settings we've got
237 if (m_pickersDirty) {
238 m_pickersDirty = false;
239 m_oneEnabledAtLeast = false;
240 m_oneHoverAtLeast = false;
241
242 const auto activeHandles = m_manager->objectPickerManager()->activeHandles();
243 for (const auto &handle : activeHandles) {
244 auto picker = m_manager->objectPickerManager()->data(handle);
245 m_oneEnabledAtLeast |= picker->isEnabled();
246 m_oneHoverAtLeast |= picker->isHoverEnabled();
247 if (m_oneEnabledAtLeast && m_oneHoverAtLeast)
248 break;
249 }
250 }
251
252 // bail out early if no picker is enabled
253 if (!m_oneEnabledAtLeast)
254 return false;
255
256 bool hasMoveEvent = false;
257 bool hasOtherEvent = false;
258 // Quickly look which types of events we've got
259 for (const auto &event : mouseEvents) {
260 const bool isMove = (event.second.type() == QEvent::MouseMove);
261 hasMoveEvent |= isMove;
262 hasOtherEvent |= !isMove;
263 }
264
265 // In the case we have a move event, find if we actually have
266 // an object picker that cares about these
267 if (!hasOtherEvent) {
268 // Retrieve the last used object picker
269 ObjectPicker *lastCurrentPicker = m_manager->objectPickerManager()->data(handle: m_currentPicker);
270
271 // The only way to set lastCurrentPicker is to click
272 // so we can return since if we're there it means we
273 // have only move events. But keep on if hover support
274 // is needed
275 if (lastCurrentPicker == nullptr && !m_oneHoverAtLeast)
276 return false;
277
278 const bool caresAboutMove = (hasMoveEvent &&
279 (m_oneHoverAtLeast ||
280 (lastCurrentPicker && lastCurrentPicker->isDragEnabled())));
281 // Early return if the current object picker doesn't care about move events
282 if (!caresAboutMove)
283 return false;
284 }
285
286 PickingUtils::ViewportCameraAreaGatherer vcaGatherer;
287 // TO DO: We could cache this and only gather when we know the FrameGraph tree has changed
288 const QVector<PickingUtils::ViewportCameraAreaDetails> vcaDetails = vcaGatherer.gather(root: m_frameGraphRoot);
289
290 // If we have no viewport / camera or area, return early
291 if (vcaDetails.empty())
292 return false;
293
294 // TO DO:
295 // If we have move or hover move events that someone cares about, we try to avoid expensive computations
296 // by compressing them into a single one
297
298 const bool trianglePickingRequested = (m_renderSettings->pickMethod() & QPickingSettings::TrianglePicking);
299 const bool edgePickingRequested = (m_renderSettings->pickMethod() & QPickingSettings::LinePicking);
300 const bool pointPickingRequested = (m_renderSettings->pickMethod() & QPickingSettings::PointPicking);
301 const bool primitivePickingRequested = pointPickingRequested | edgePickingRequested | trianglePickingRequested;
302 const bool frontFaceRequested =
303 m_renderSettings->faceOrientationPickingMode() != QPickingSettings::BackFace;
304 const bool backFaceRequested =
305 m_renderSettings->faceOrientationPickingMode() != QPickingSettings::FrontFace;
306 const float pickWorldSpaceTolerance = m_renderSettings->pickWorldSpaceTolerance();
307
308 // For each mouse event
309 for (const auto &event : mouseEvents) {
310 m_hoveredPickersToClear = m_hoveredPickers;
311
312 QPickEvent::Buttons eventButton = QPickEvent::NoButton;
313 int eventButtons = 0;
314 int eventModifiers = QPickEvent::NoModifier;
315
316 setEventButtonAndModifiers(event: event.second, eventButton, eventButtons, eventModifiers);
317
318 // For each Viewport / Camera and Area entry
319 for (const PickingUtils::ViewportCameraAreaDetails &vca : vcaDetails) {
320 PickingUtils::HitList sphereHits;
321 QRay3D ray = rayForViewportAndCamera(vca, eventSource: event.first, pos: event.second.pos());
322 if (!ray.isValid()) {
323 // An invalid rays is when we've lost our surface or the mouse
324 // has moved out of the viewport In case of a button released
325 // outside of the viewport, we still want to notify the
326 // lastCurrent entity about this.
327 dispatchPickEvents(event: event.second, sphereHits: PickingUtils::HitList(), eventButton, eventButtons, eventModifiers, allHitsRequested: m_renderSettings->pickResultMode(),
328 viewportNodeId: vca.viewportNodeId);
329 continue;
330 }
331
332 PickingUtils::HierarchicalEntityPicker entityPicker(ray);
333 entityPicker.setLayerFilterIds(vca.layersFilters);
334 if (entityPicker.collectHits(manager: m_manager, root: m_node)) {
335 if (trianglePickingRequested) {
336 PickingUtils::TriangleCollisionGathererFunctor gathererFunctor;
337 gathererFunctor.m_frontFaceRequested = frontFaceRequested;
338 gathererFunctor.m_backFaceRequested = backFaceRequested;
339 gathererFunctor.m_manager = m_manager;
340 gathererFunctor.m_ray = ray;
341 gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
342 sphereHits << gathererFunctor.computeHits(entities: entityPicker.entities(), mode: m_renderSettings->pickResultMode());
343 }
344 if (edgePickingRequested) {
345 PickingUtils::LineCollisionGathererFunctor gathererFunctor;
346 gathererFunctor.m_manager = m_manager;
347 gathererFunctor.m_ray = ray;
348 gathererFunctor.m_pickWorldSpaceTolerance = pickWorldSpaceTolerance;
349 gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
350 sphereHits << gathererFunctor.computeHits(entities: entityPicker.entities(), mode: m_renderSettings->pickResultMode());
351 PickingUtils::AbstractCollisionGathererFunctor::sortHits(results&: sphereHits);
352 }
353 if (pointPickingRequested) {
354 PickingUtils::PointCollisionGathererFunctor gathererFunctor;
355 gathererFunctor.m_manager = m_manager;
356 gathererFunctor.m_ray = ray;
357 gathererFunctor.m_pickWorldSpaceTolerance = pickWorldSpaceTolerance;
358 gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
359 sphereHits << gathererFunctor.computeHits(entities: entityPicker.entities(), mode: m_renderSettings->pickResultMode());
360 PickingUtils::AbstractCollisionGathererFunctor::sortHits(results&: sphereHits);
361 }
362 if (!primitivePickingRequested) {
363 sphereHits << entityPicker.hits();
364 PickingUtils::AbstractCollisionGathererFunctor::sortHits(results&: sphereHits);
365 if (m_renderSettings->pickResultMode() != QPickingSettings::AllPicks)
366 sphereHits = { sphereHits.front() };
367 }
368 }
369
370 // Dispatch events based on hit results
371 dispatchPickEvents(event: event.second, sphereHits, eventButton, eventButtons, eventModifiers, allHitsRequested: m_renderSettings->pickResultMode(),
372 viewportNodeId: vca.viewportNodeId);
373 }
374 }
375
376 // Clear Hovered elements that needs to be cleared
377 // Send exit event to object pickers on which we
378 // had set the hovered flag for a previous frame
379 // and that aren't being hovered any longer
380 clearPreviouslyHoveredPickers();
381 return true;
382}
383
384void PickBoundingVolumeJob::dispatchPickEvents(const QMouseEvent &event,
385 const PickingUtils::HitList &sphereHits,
386 QPickEvent::Buttons eventButton,
387 int eventButtons,
388 int eventModifiers,
389 bool allHitsRequested,
390 Qt3DCore::QNodeId viewportNodeId)
391{
392 Q_D(PickBoundingVolumeJob);
393
394 ObjectPicker *lastCurrentPicker = m_manager->objectPickerManager()->data(handle: m_currentPicker);
395 // If we have hits
396 if (!sphereHits.isEmpty()) {
397 // Note: how can we control that we want the first/last/all elements along the ray to be picked
398
399 // How do we differentiate betwnee an Entity with no GeometryRenderer and one with one, both having
400 // an ObjectPicker component when it comes
401
402 for (const QCollisionQueryResult::Hit &hit : qAsConst(t: sphereHits)) {
403 Entity *entity = m_manager->renderNodesManager()->lookupResource(id: hit.m_entityId);
404 HObjectPicker objectPickerHandle = entity->componentHandle<ObjectPicker>();
405
406 // If the Entity which actually received the hit doesn't have
407 // an object picker component, we need to check the parent if it has one ...
408 while (objectPickerHandle.isNull() && entity != nullptr) {
409 entity = entity->parent();
410 if (entity != nullptr)
411 objectPickerHandle = entity->componentHandle<ObjectPicker>();
412 }
413
414 ObjectPicker *objectPicker = m_manager->objectPickerManager()->data(handle: objectPickerHandle);
415 if (objectPicker != nullptr && objectPicker->isEnabled()) {
416
417 if (lastCurrentPicker && !allHitsRequested) {
418 // if there is a current picker, it will "grab" all events until released.
419 // Clients should test that entity is what they expect (or only use
420 // world coordinates)
421 objectPicker = lastCurrentPicker;
422 }
423
424 // Send the corresponding event
425 Vector3D localIntersection = hit.m_intersection;
426 if (entity && entity->worldTransform())
427 localIntersection = entity->worldTransform()->inverted() * hit.m_intersection;
428
429 QPickEventPtr pickEvent;
430 switch (hit.m_type) {
431 case QCollisionQueryResult::Hit::Triangle:
432 pickEvent.reset(t: new QPickTriangleEvent(event.localPos(),
433 convertToQVector3D(v: hit.m_intersection),
434 convertToQVector3D(v: localIntersection),
435 hit.m_distance,
436 hit.m_primitiveIndex,
437 hit.m_vertexIndex[0],
438 hit.m_vertexIndex[1],
439 hit.m_vertexIndex[2],
440 eventButton, eventButtons,
441 eventModifiers,
442 convertToQVector3D(v: hit.m_uvw)));
443 break;
444 case QCollisionQueryResult::Hit::Edge:
445 pickEvent.reset(t: new QPickLineEvent(event.localPos(),
446 convertToQVector3D(v: hit.m_intersection),
447 convertToQVector3D(v: localIntersection),
448 hit.m_distance,
449 hit.m_primitiveIndex,
450 hit.m_vertexIndex[0], hit.m_vertexIndex[1],
451 eventButton, eventButtons, eventModifiers));
452 break;
453 case QCollisionQueryResult::Hit::Point:
454 pickEvent.reset(t: new QPickPointEvent(event.localPos(),
455 convertToQVector3D(v: hit.m_intersection),
456 convertToQVector3D(v: localIntersection),
457 hit.m_distance,
458 hit.m_vertexIndex[0],
459 eventButton, eventButtons, eventModifiers));
460 break;
461 case QCollisionQueryResult::Hit::Entity:
462 pickEvent.reset(t: new QPickEvent(event.localPos(),
463 convertToQVector3D(v: hit.m_intersection),
464 convertToQVector3D(v: localIntersection),
465 hit.m_distance,
466 eventButton, eventButtons, eventModifiers));
467 break;
468 default:
469 Q_UNREACHABLE();
470 }
471 Qt3DRender::QPickEventPrivate::get(object: pickEvent.data())->m_entity = hit.m_entityId;
472 switch (event.type()) {
473 case QEvent::MouseButtonPress: {
474 // Store pressed object handle
475 m_currentPicker = objectPickerHandle;
476 m_currentViewport = viewportNodeId;
477 // Send pressed event to m_currentPicker
478 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(), .sourceEventType: event.type(), .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
479 objectPicker->setPressed(true);
480 break;
481 }
482
483 case QEvent::MouseButtonRelease: {
484 // Only send the release event if it was pressed
485 if (objectPicker->isPressed()) {
486 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(), .sourceEventType: event.type(), .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
487 objectPicker->setPressed(false);
488 }
489 if (lastCurrentPicker != nullptr && m_currentPicker == objectPickerHandle) {
490 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(),
491 .sourceEventType: PickBoundingVolumeJobPrivate::MouseButtonClick,
492 .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
493 m_currentPicker = HObjectPicker();
494 m_currentViewport = {};
495 }
496 break;
497 }
498#if QT_CONFIG(gestures)
499 case QEvent::Gesture: {
500 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(),
501 .sourceEventType: PickBoundingVolumeJobPrivate::MouseButtonClick,
502 .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
503 break;
504 }
505#endif
506 case QEvent::MouseMove: {
507 if ((objectPicker->isPressed() || objectPicker->isHoverEnabled()) && objectPicker->isDragEnabled())
508 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(), .sourceEventType: event.type(), .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
509 Q_FALLTHROUGH(); // fallthrough
510 }
511 case QEvent::HoverMove: {
512 if (!m_hoveredPickers.contains(t: objectPickerHandle)) {
513 if (objectPicker->isHoverEnabled()) {
514 // Send entered event to objectPicker
515 d->dispatches.push_back(t: {.pickerId: objectPicker->peerId(), .sourceEventType: QEvent::Enter, .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
516 // and save it in the hoveredPickers
517 m_hoveredPickers.push_back(t: objectPickerHandle);
518 }
519 }
520 break;
521 }
522
523 default:
524 break;
525 }
526 }
527
528 // The ObjectPicker was hit -> it is still being hovered
529 m_hoveredPickersToClear.removeAll(t: objectPickerHandle);
530
531 lastCurrentPicker = m_manager->objectPickerManager()->data(handle: m_currentPicker);
532 }
533
534 // Otherwise no hits
535 } else {
536 switch (event.type()) {
537 case QEvent::MouseButtonRelease: {
538 // Send release event to m_currentPicker
539 if (lastCurrentPicker != nullptr && m_currentViewport == viewportNodeId) {
540 m_currentPicker = HObjectPicker();
541 m_currentViewport = {};
542 QPickEventPtr pickEvent(new QPickEvent);
543 lastCurrentPicker->setPressed(false);
544 d->dispatches.push_back(t: {.pickerId: lastCurrentPicker->peerId(), .sourceEventType: event.type(), .resultingEvent: pickEvent, .viewportNodeId: viewportNodeId});
545 }
546 break;
547 }
548 default:
549 break;
550 }
551 }
552}
553
554void PickBoundingVolumeJob::clearPreviouslyHoveredPickers()
555{
556 Q_D(PickBoundingVolumeJob);
557
558 for (const HObjectPicker &pickHandle : qAsConst(t&: m_hoveredPickersToClear)) {
559 ObjectPicker *pick = m_manager->objectPickerManager()->data(handle: pickHandle);
560 if (pick)
561 d->dispatches.push_back(t: {.pickerId: pick->peerId(), .sourceEventType: QEvent::Leave, .resultingEvent: {}, .viewportNodeId: {}});
562 m_hoveredPickers.removeAll(t: pickHandle);
563 }
564
565 m_hoveredPickersToClear.clear();
566}
567
568} // namespace Render
569
570} // namespace Qt3DRender
571
572QT_END_NAMESPACE
573

source code of qt3d/src/render/jobs/pickboundingvolumejob.cpp