// Copyright (C) 2016 Klaralvdalens Datakonsult AB (KDAB). // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "pickboundingvolumeutils_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE namespace Qt3DRender { using namespace Qt3DRender::RayCasting; namespace Render { namespace PickingUtils { PickConfiguration::PickConfiguration(FrameGraphNode *frameGraphRoot, RenderSettings *renderSettings) { ViewportCameraAreaGatherer vcaGatherer; // TO DO: We could cache this and only gather when we know the FrameGraph tree has changed vcaDetails = vcaGatherer.gather(frameGraphRoot); // If we have no viewport / camera or area, return early if (vcaDetails.empty()) return; // TO DO: // If we have move or hover move events that someone cares about, we try to avoid expensive computations // by compressing them into a single one trianglePickingRequested = (renderSettings->pickMethod() & QPickingSettings::TrianglePicking); edgePickingRequested = (renderSettings->pickMethod() & QPickingSettings::LinePicking); pointPickingRequested = (renderSettings->pickMethod() & QPickingSettings::PointPicking); primitivePickingRequested = pointPickingRequested | edgePickingRequested | trianglePickingRequested; frontFaceRequested = renderSettings->faceOrientationPickingMode() != QPickingSettings::BackFace; backFaceRequested = renderSettings->faceOrientationPickingMode() != QPickingSettings::FrontFace; pickWorldSpaceTolerance = renderSettings->pickWorldSpaceTolerance(); } void ViewportCameraAreaGatherer::visit(FrameGraphNode *node) { const auto children = node->children(); for (Render::FrameGraphNode *n : children) visit(n); if (node->childrenIds().empty()) m_leaves.push_back(node); } ViewportCameraAreaDetails ViewportCameraAreaGatherer::gatherUpViewportCameraAreas(Render::FrameGraphNode *node) const { ViewportCameraAreaDetails vca; vca.viewport = QRectF(0., 0., 1., 1.); while (node) { if (node->isEnabled()) { switch (node->nodeType()) { case FrameGraphNode::CameraSelector: vca.cameraId = static_cast(node)->cameraUuid(); break; case FrameGraphNode::Viewport: { auto vnode = static_cast(node); // we want the leaf viewport so if we have a viewport node already don't override it with its parent if (!vca.viewportNodeId) vca.viewportNodeId = vnode->peerId(); vca.viewport = ViewportNode::computeViewport(vca.viewport, vnode); break; } case FrameGraphNode::Surface: { auto selector = static_cast(node); vca.area = selector->renderTargetSize(); vca.surface = selector->surface(); break; } case FrameGraphNode::NoPicking: { // Return an empty/invalid ViewportCameraAreaDetails which will // prevent picking in the presence of a NoPicking node return {}; } case FrameGraphNode::LayerFilter: { auto fnode = static_cast(node); vca.layersFilters.push_back(fnode->peerId()); break; } default: break; } } node = node->parent(); } return vca; } std::vector ViewportCameraAreaGatherer::gather(FrameGraphNode *root) { // Retrieve all leaves visit(root); std::vector vcaTriplets; vcaTriplets.reserve(m_leaves.size()); // Find all viewport/camera pairs by traversing from leaf to root for (Render::FrameGraphNode *leaf : m_leaves) { ViewportCameraAreaDetails vcaDetails = gatherUpViewportCameraAreas(leaf); if (!m_targetCamera.isNull() && vcaDetails.cameraId != m_targetCamera) continue; if (!vcaDetails.cameraId.isNull() && isUnique(vcaTriplets, vcaDetails)) vcaTriplets.push_back(vcaDetails); } return vcaTriplets; } bool ViewportCameraAreaGatherer::isUnique(const std::vector &vcaList, const ViewportCameraAreaDetails &vca) const { for (const ViewportCameraAreaDetails &listItem : vcaList) { if (vca.cameraId == listItem.cameraId && vca.viewport == listItem.viewport && vca.surface == listItem.surface && vca.area == listItem.area && vca.layersFilters == listItem.layersFilters) return false; } return true; } class TriangleCollisionVisitor : public TrianglesVisitor { public: HitList hits; TriangleCollisionVisitor(NodeManagers* manager, const Entity *root, const RayCasting::QRay3D& ray, bool frontFaceRequested, bool backFaceRequested) : TrianglesVisitor(manager), m_root(root), m_ray(ray), m_triangleIndex(0) , m_frontFaceRequested(frontFaceRequested), m_backFaceRequested(backFaceRequested) { } private: const Entity *m_root; RayCasting::QRay3D m_ray; uint m_triangleIndex; bool m_frontFaceRequested; bool m_backFaceRequested; void visit(uint andx, const Vector3D &a, uint bndx, const Vector3D &b, uint cndx, const Vector3D &c) override; bool intersectsSegmentTriangle(uint andx, const Vector3D &a, uint bndx, const Vector3D &b, uint cndx, const Vector3D &c); }; void TriangleCollisionVisitor::visit(uint andx, const Vector3D &a, uint bndx, const Vector3D &b, uint cndx, const Vector3D &c) { const Matrix4x4 &mat = *m_root->worldTransform(); const Vector3D tA = mat.map(a); const Vector3D tB = mat.map(b); const Vector3D tC = mat.map(c); bool intersected = m_frontFaceRequested && intersectsSegmentTriangle(cndx, tC, bndx, tB, andx, tA); // front facing if (!intersected && m_backFaceRequested) { intersected = intersectsSegmentTriangle(andx, tA, bndx, tB, cndx, tC); // back facing } m_triangleIndex++; } bool TriangleCollisionVisitor::intersectsSegmentTriangle(uint andx, const Vector3D &a, uint bndx, const Vector3D &b, uint cndx, const Vector3D &c) { float t = 0.0f; Vector3D uvw; bool intersected = Render::intersectsSegmentTriangle(m_ray, a, b, c, uvw, t); if (intersected) { QCollisionQueryResult::Hit queryResult; queryResult.m_type = QCollisionQueryResult::Hit::Triangle; queryResult.m_entityId = m_root->peerId(); queryResult.m_primitiveIndex = m_triangleIndex; queryResult.m_vertexIndex[0] = andx; queryResult.m_vertexIndex[1] = bndx; queryResult.m_vertexIndex[2] = cndx; queryResult.m_uvw = uvw; queryResult.m_intersection = m_ray.point(t * m_ray.distance()); queryResult.m_distance = m_ray.projectedDistance(queryResult.m_intersection); hits.push_back(queryResult); } return intersected; } class LineCollisionVisitor : public SegmentsVisitor { public: HitList hits; LineCollisionVisitor(NodeManagers* manager, const Entity *root, const RayCasting::QRay3D& ray, float pickWorldSpaceTolerance) : SegmentsVisitor(manager), m_root(root), m_ray(ray) , m_segmentIndex(0), m_pickWorldSpaceTolerance(pickWorldSpaceTolerance) { } private: const Entity *m_root; RayCasting::QRay3D m_ray; uint m_segmentIndex; float m_pickWorldSpaceTolerance; void visit(uint andx, const Vector3D &a, uint bndx, const Vector3D &b) override; bool intersectsSegmentSegment(uint andx, const Vector3D &a, uint bndx, const Vector3D &b); bool rayToLineSegment(const Vector3D& lineStart,const Vector3D& lineEnd, float &distance, Vector3D &intersection) const; }; void LineCollisionVisitor::visit(uint andx, const Vector3D &a, uint bndx, const Vector3D &b) { const Matrix4x4 &mat = *m_root->worldTransform(); const Vector3D tA = mat.map(a); const Vector3D tB = mat.map(b); intersectsSegmentSegment(andx, tA, bndx, tB); m_segmentIndex++; } bool LineCollisionVisitor::intersectsSegmentSegment(uint andx, const Vector3D &a, uint bndx, const Vector3D &b) { float distance = 0.f; Vector3D intersection; bool res = rayToLineSegment(a, b, distance, intersection); if (res) { QCollisionQueryResult::Hit queryResult; queryResult.m_type = QCollisionQueryResult::Hit::Edge; queryResult.m_entityId = m_root->peerId(); queryResult.m_primitiveIndex = m_segmentIndex; queryResult.m_vertexIndex[0] = andx; queryResult.m_vertexIndex[1] = bndx; queryResult.m_intersection = intersection; queryResult.m_distance = m_ray.projectedDistance(queryResult.m_intersection); hits.push_back(queryResult); return true; } return false; } bool LineCollisionVisitor::rayToLineSegment(const Vector3D& lineStart,const Vector3D& lineEnd, float &distance, Vector3D &intersection) const { const float epsilon = 0.00000001f; const Vector3D u = m_ray.direction() * m_ray.distance(); const Vector3D v = lineEnd - lineStart; const Vector3D w = m_ray.origin() - lineStart; const float a = Vector3D::dotProduct(u, u); const float b = Vector3D::dotProduct(u, v); const float c = Vector3D::dotProduct(v, v); const float d = Vector3D::dotProduct(u, w); const float e = Vector3D::dotProduct(v, w); const float D = a * c - b * b; float sc, sN, sD = D; float tc, tN, tD = D; if (D < epsilon) { sN = 0.0; sD = 1.0; tN = e; tD = c; } else { sN = (b * e - c * d); tN = (a * e - b * d); if (sN < 0.0) { sN = 0.0; tN = e; tD = c; } } if (tN < 0.0) { tN = 0.0; if (-d < 0.0) sN = 0.0; else { sN = -d; sD = a; } } else if (tN > tD) { tN = tD; if ((-d + b) < 0.0) sN = 0; else { sN = (-d + b); sD = a; } } sc = (qAbs(sN) < epsilon ? 0.0f : sN / sD); tc = (qAbs(tN) < epsilon ? 0.0f : tN / tD); const Vector3D dP = w + (sc * u) - (tc * v); const float f = dP.length(); if (f < m_pickWorldSpaceTolerance) { distance = sc * u.length(); intersection = lineStart + v * tc; return true; } return false; } class PointCollisionVisitor : public PointsVisitor { public: HitList hits; PointCollisionVisitor(NodeManagers* manager, const Entity *root, const RayCasting::QRay3D& ray, float pickWorldSpaceTolerance) : PointsVisitor(manager), m_root(root), m_ray(ray) , m_pointIndex(0), m_pickWorldSpaceTolerance(pickWorldSpaceTolerance) { } private: const Entity *m_root; RayCasting::QRay3D m_ray; uint m_pointIndex; float m_pickWorldSpaceTolerance; void visit(uint ndx, const Vector3D &p) override; double pointToRayDistance(const Vector3D &a, Vector3D &p) { const Vector3D v = a - m_ray.origin(); const double t = Vector3D::dotProduct(v, m_ray.direction()); p = m_ray.origin() + t * m_ray.direction(); return (p - a).length(); } }; void PointCollisionVisitor::visit(uint ndx, const Vector3D &p) { const Matrix4x4 &mat = *m_root->worldTransform(); const Vector3D tP = mat.map(p); Vector3D intersection; float d = pointToRayDistance(tP, intersection); if (d < m_pickWorldSpaceTolerance) { QCollisionQueryResult::Hit queryResult; queryResult.m_type = QCollisionQueryResult::Hit::Point; queryResult.m_entityId = m_root->peerId(); queryResult.m_primitiveIndex = m_pointIndex; queryResult.m_vertexIndex[0] = ndx; queryResult.m_intersection = intersection; queryResult.m_distance = d; hits.push_back(queryResult); } m_pointIndex++; } HitList reduceToFirstHit(HitList &result, const HitList &intermediate) { if (!intermediate.empty()) { if (result.empty()) result.push_back(intermediate.front()); float closest = result.front().m_distance; for (const auto &v : intermediate) { if (v.m_distance < closest) { result.insert(result.begin(), v); closest = v.m_distance; } } while (result.size() > 1) result.pop_back(); } return result; } struct HighestPriorityHitReducer { // No need to protect this from concurrent access as the table // is read only const QHash entityToPriorityTable; HitList operator()(HitList &result, const HitList &intermediate) { // Sort by priority first // If we have equal priorities, we then sort by distance if (!intermediate.empty()) { if (result.empty()) result.push_back(intermediate.front()); int currentPriority = entityToPriorityTable.value(result.front().m_entityId, 0); float closest = result.front().m_distance; for (const auto &v : intermediate) { const int newEntryPriority = entityToPriorityTable.value(v.m_entityId, 0); if (newEntryPriority > currentPriority) { result.insert(result.begin(), v); currentPriority = newEntryPriority; closest = v.m_distance; } else if (newEntryPriority == currentPriority) { if (v.m_distance < closest) { result.insert(result.begin(), v); closest = v.m_distance; currentPriority = newEntryPriority; } } } while (result.size() > 1) result.pop_back(); } return result; } }; HitList reduceToAllHits(HitList &results, const HitList &intermediate) { if (!intermediate.empty()) results.insert(results.end(), std::make_move_iterator(intermediate.begin()), std::make_move_iterator(intermediate.end())); return results; } AbstractCollisionGathererFunctor::AbstractCollisionGathererFunctor() : m_manager(nullptr) { } AbstractCollisionGathererFunctor::~AbstractCollisionGathererFunctor() { } HitList AbstractCollisionGathererFunctor::operator ()(const Entity *entity) const { if (m_objectPickersRequired) { HObjectPicker objectPickerHandle = entity->componentHandle(); // If the Entity which actually received the hit doesn't have // an object picker component, we need to check the parent if it has one ... auto parentEntity = entity; while (objectPickerHandle.isNull() && parentEntity != nullptr) { parentEntity = parentEntity->parent(); if (parentEntity != nullptr) objectPickerHandle = parentEntity->componentHandle(); } ObjectPicker *objectPicker = m_manager->objectPickerManager()->data(objectPickerHandle); if (objectPicker == nullptr || !objectPicker->isEnabled()) return {}; // don't bother picking entities that don't // have an object picker, or if it's disabled } return pick(entity); } bool AbstractCollisionGathererFunctor::rayHitsEntity(const Entity *entity) const { QRayCastingService rayCasting; const QCollisionQueryResult::Hit queryResult = rayCasting.query(m_ray, entity->worldBoundingVolume()); return queryResult.m_distance >= 0.f; } void AbstractCollisionGathererFunctor::sortHits(HitList &results) { auto compareHitsDistance = [](const HitList::value_type &a, const HitList::value_type &b) { return a.m_distance < b.m_distance; }; std::sort(results.begin(), results.end(), compareHitsDistance); } namespace { // Workaround to avoid passing *this into the blockMappedReduce calls for the // mapFunctor which would cause an SSE alignment error on Windows Also note // that a lambda doesn't work since we need the typedef result_type defined to // work with QtConcurrent struct MapFunctorHolder { MapFunctorHolder(const AbstractCollisionGathererFunctor *gatherer) : m_gatherer(gatherer) {} // This define is required to work with QtConcurrent typedef HitList result_type; HitList operator ()(const Entity *e) const { return m_gatherer->operator ()(e); } const AbstractCollisionGathererFunctor *m_gatherer; }; } // anonymous HitList EntityCollisionGathererFunctor::computeHits(const std::vector &entities, Qt3DRender::QPickingSettings::PickResultMode mode) { std::function reducerOp; switch (mode) { case QPickingSettings::AllPicks: reducerOp = PickingUtils::reduceToAllHits; break; case QPickingSettings::NearestPriorityPick: reducerOp = HighestPriorityHitReducer{ m_entityToPriorityTable }; break; case QPickingSettings::NearestPick: reducerOp = PickingUtils::reduceToFirstHit; break; } const MapFunctorHolder holder(this); #if QT_CONFIG(concurrent) return QtConcurrent::blockingMappedReduced(entities, holder, reducerOp); #else HitList sphereHits; QList results; for (const Entity *entity : entities) sphereHits = reducerOp(sphereHits, holder(entity)); return sphereHits; #endif } HitList EntityCollisionGathererFunctor::pick(const Entity *entity) const { HitList result; QRayCastingService rayCasting; const QCollisionQueryResult::Hit queryResult = rayCasting.query(m_ray, entity->worldBoundingVolume()); if (queryResult.m_distance >= 0.f) result.push_back(queryResult); return result; } HitList TriangleCollisionGathererFunctor::computeHits(const std::vector &entities, Qt3DRender::QPickingSettings::PickResultMode mode) { std::function reducerOp; switch (mode) { case QPickingSettings::AllPicks: reducerOp = PickingUtils::reduceToAllHits; break; case QPickingSettings::NearestPriorityPick: reducerOp = HighestPriorityHitReducer { m_entityToPriorityTable }; break; case QPickingSettings::NearestPick: reducerOp = PickingUtils::reduceToFirstHit; break; } const MapFunctorHolder holder(this); #if QT_CONFIG(concurrent) return QtConcurrent::blockingMappedReduced(entities, holder, reducerOp); #else HitList sphereHits; QList results; for (const Entity *entity : entities) sphereHits = reducerOp(sphereHits, holder(entity)); return sphereHits; #endif } HitList TriangleCollisionGathererFunctor::pick(const Entity *entity) const { HitList result; PickingProxy *proxy = entity->renderComponent(); if (proxy && proxy->isEnabled() && proxy->isValid()) { if (rayHitsEntity(entity)) { TriangleCollisionVisitor visitor(m_manager, entity, m_ray, m_frontFaceRequested, m_backFaceRequested); visitor.apply(proxy, entity->peerId()); result = visitor.hits; sortHits(result); } } else { GeometryRenderer *gRenderer = entity->renderComponent(); if (!gRenderer || !gRenderer->isEnabled()) return result; if (rayHitsEntity(entity)) { TriangleCollisionVisitor visitor(m_manager, entity, m_ray, m_frontFaceRequested, m_backFaceRequested); visitor.apply(gRenderer, entity->peerId()); result = visitor.hits; sortHits(result); } } return result; } HitList LineCollisionGathererFunctor::computeHits(const std::vector &entities, Qt3DRender::QPickingSettings::PickResultMode mode) { std::function reducerOp; switch (mode) { case QPickingSettings::AllPicks: reducerOp = PickingUtils::reduceToAllHits; break; case QPickingSettings::NearestPriorityPick: reducerOp = HighestPriorityHitReducer { m_entityToPriorityTable }; break; case QPickingSettings::NearestPick: reducerOp = PickingUtils::reduceToFirstHit; break; } const MapFunctorHolder holder(this); #if QT_CONFIG(concurrent) return QtConcurrent::blockingMappedReduced(entities, holder, reducerOp); #else HitList sphereHits; QList results; for (const Entity *entity : entities) sphereHits = reducerOp(sphereHits, holder(entity)); return sphereHits; #endif } HitList LineCollisionGathererFunctor::pick(const Entity *entity) const { HitList result; PickingProxy *proxy = entity->renderComponent(); if (proxy && proxy->isEnabled() && proxy->isValid()) { if (rayHitsEntity(entity)) { LineCollisionVisitor visitor(m_manager, entity, m_ray, m_pickWorldSpaceTolerance); visitor.apply(proxy, entity->peerId()); result = visitor.hits; sortHits(result); } } else { GeometryRenderer *gRenderer = entity->renderComponent(); if (!gRenderer) return result; if (rayHitsEntity(entity)) { LineCollisionVisitor visitor(m_manager, entity, m_ray, m_pickWorldSpaceTolerance); visitor.apply(gRenderer, entity->peerId()); result = visitor.hits; sortHits(result); } } return result; } HitList PointCollisionGathererFunctor::computeHits(const std::vector &entities, Qt3DRender::QPickingSettings::PickResultMode mode) { std::function reducerOp; switch (mode) { case QPickingSettings::AllPicks: reducerOp = PickingUtils::reduceToAllHits; break; case QPickingSettings::NearestPriorityPick: reducerOp = HighestPriorityHitReducer { m_entityToPriorityTable }; break; case QPickingSettings::NearestPick: reducerOp = PickingUtils::reduceToFirstHit; break; } const MapFunctorHolder holder(this); #if QT_CONFIG(concurrent) return QtConcurrent::blockingMappedReduced(entities, holder, reducerOp); #else HitList sphereHits; QList results; for (const Entity *entity : entities) sphereHits = reducerOp(sphereHits, holder(entity)); return sphereHits; #endif } HitList PointCollisionGathererFunctor::pick(const Entity *entity) const { HitList result; PickingProxy *proxy = entity->renderComponent(); if (proxy && proxy->isEnabled() && proxy->isValid() && proxy->primitiveType() != Qt3DCore::QGeometryView::Points) { if (rayHitsEntity(entity)) { PointCollisionVisitor visitor(m_manager, entity, m_ray, m_pickWorldSpaceTolerance); visitor.apply(proxy, entity->peerId()); result = visitor.hits; sortHits(result); } } else { GeometryRenderer *gRenderer = entity->renderComponent(); if (!gRenderer) return result; if (gRenderer->primitiveType() != Qt3DRender::QGeometryRenderer::Points) return result; if (rayHitsEntity(entity)) { PointCollisionVisitor visitor(m_manager, entity, m_ray, m_pickWorldSpaceTolerance); visitor.apply(gRenderer, entity->peerId()); result = visitor.hits; sortHits(result); } } return result; } HierarchicalEntityPicker::HierarchicalEntityPicker(const QRay3D &ray, bool requireObjectPicker) : m_ray(ray) , m_objectPickersRequired(requireObjectPicker) { } void HierarchicalEntityPicker::setLayerFilterIds(const Qt3DCore::QNodeIdVector &layerFilterIds) { m_layerFilterIds = layerFilterIds; } void HierarchicalEntityPicker::setLayerIds(const Qt3DCore::QNodeIdVector &layerIds, QAbstractRayCaster::FilterMode mode) { m_layerIds = layerIds; m_layerFilterMode = mode; } bool HierarchicalEntityPicker::collectHits(NodeManagers *manager, Entity *root) { m_hits.clear(); m_entities.clear(); m_entityToPriorityTable.clear(); QRayCastingService rayCasting; struct EntityData { Entity* entity; bool hasObjectPicker; int priority; }; std::vector worklist; worklist.push_back({root, !root->componentHandle().isNull(), 0}); // Record all entities that satisfy layerFiltering. We can then check against // that to see if a picked Entity also satisfies the layer filtering // Note: PickBoundingVolumeJob filters against LayerFilter nodes (FG) whereas // the RayCastingJob filters only against a set of Layers and a filter Mode const bool hasLayerFilters = m_layerFilterIds.size() > 0; const bool hasLayers = m_layerIds.size() > 0; const bool hasLayerFiltering = hasLayerFilters || hasLayers; std::vector layerFilterEntities; FilterLayerEntityJob layerFilterJob; layerFilterJob.setManager(manager); if (hasLayerFilters) { // Note: we expect UpdateEntityLayersJob was called beforehand to handle layer recursivness // Filtering against LayerFilters (PickBoundingVolumeJob) if (m_layerFilterIds.size()) { layerFilterJob.setLayerFilters(m_layerFilterIds); layerFilterJob.run(); layerFilterEntities = layerFilterJob.filteredEntities(); } } while (!worklist.empty()) { EntityData current = worklist.back(); worklist.pop_back(); // first pick entry sub-scene-graph QCollisionQueryResult::Hit queryResult = rayCasting.query(m_ray, current.entity->worldBoundingVolumeWithChildren()); if (queryResult.m_distance < 0.f) continue; // if we get a hit, we check again for this specific entity queryResult = rayCasting.query(m_ray, current.entity->worldBoundingVolume()); // Check Entity is in selected Layers if we have LayerIds or LayerFilterIds // Note: it's not because a parent doesn't satisfy the layerFiltering that a child might not. // Therefore we need to keep traversing children in all cases // Are we filtering against layerIds (RayCastingJob) if (hasLayers) { // QLayerFilter::FilterMode and QAbstractRayCaster::FilterMode are the same layerFilterJob.filterEntityAgainstLayers(current.entity, m_layerIds, static_cast(m_layerFilterMode)); layerFilterEntities = layerFilterJob.filteredEntities(); } const bool isInLayers = !hasLayerFiltering || Qt3DCore::contains(layerFilterEntities, current.entity); if (isInLayers && queryResult.m_distance >= 0.f && (current.hasObjectPicker || !m_objectPickersRequired)) { m_entities.push_back(current.entity); m_hits.push_back(queryResult); // Record entry for entity/priority m_entityToPriorityTable.insert(current.entity->peerId(), current.priority); } // and pick children const auto &childrenHandles = current.entity->childrenHandles(); for (const HEntity &handle : childrenHandles) { Entity *child = manager->renderNodesManager()->data(handle); if (child) { ObjectPicker *childPicker = child->renderComponent(); worklist.push_back({child, current.hasObjectPicker || childPicker, (childPicker ? childPicker->priority() : current.priority)}); } } } return !m_hits.empty(); } } // PickingUtils } // Render } // Qt3DRender QT_END_NAMESPACE