// Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB). // Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "calcboundingvolumejob_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_CONFIG(concurrent) #include #endif #include QT_BEGIN_NAMESPACE namespace Qt3DRender { namespace Render { using namespace Qt3DCore; namespace { class BoundingVolumeCalculator { public: explicit BoundingVolumeCalculator(NodeManagers *manager) : m_manager(manager) { } const Sphere& result() { return m_volume; } const QVector3D min() const { return m_min; } const QVector3D max() const { return m_max; } bool apply(Qt3DRender::Render::Attribute *positionAttribute, Qt3DRender::Render::Attribute *indexAttribute, int drawVertexCount, bool primitiveRestartEnabled, int primitiveRestartIndex) { FindExtremePoints findExtremePoints(m_manager); if (!findExtremePoints.apply(positionAttribute, indexAttribute, drawVertexCount, primitiveRestartEnabled, primitiveRestartIndex)) return false; m_min = QVector3D(findExtremePoints.xMin, findExtremePoints.yMin, findExtremePoints.zMin); m_max = QVector3D(findExtremePoints.xMax, findExtremePoints.yMax, findExtremePoints.zMax); FindMaxDistantPoint maxDistantPointY(m_manager); maxDistantPointY.setReferencePoint = true; if (!maxDistantPointY.apply(positionAttribute, indexAttribute, drawVertexCount, primitiveRestartEnabled, primitiveRestartIndex)) return false; if (maxDistantPointY.hasNoPoints) return false; //const Vector3D x = maxDistantPointY.referencePt; const Vector3D y = maxDistantPointY.maxDistPt; FindMaxDistantPoint maxDistantPointZ(m_manager); maxDistantPointZ.setReferencePoint = false; maxDistantPointZ.referencePt = y; if (!maxDistantPointZ.apply(positionAttribute, indexAttribute, drawVertexCount, primitiveRestartEnabled, primitiveRestartIndex)) { return false; } const Vector3D z = maxDistantPointZ.maxDistPt; const Vector3D center = (y + z) * 0.5f; FindMaxDistantPoint maxDistantPointCenter(m_manager); maxDistantPointCenter.setReferencePoint = false; maxDistantPointCenter.referencePt = center; if (!maxDistantPointCenter.apply(positionAttribute, indexAttribute, drawVertexCount, primitiveRestartEnabled, primitiveRestartIndex)) { return false; } const float radius = (center - maxDistantPointCenter.maxDistPt).length(); m_volume = Qt3DRender::Render::Sphere(center, radius); if (m_volume.isNull()) return false; return true; } private: Sphere m_volume; NodeManagers *m_manager; QVector3D m_min; QVector3D m_max; class FindExtremePoints : public Buffer3fVisitor { public: explicit FindExtremePoints(NodeManagers *manager) : Buffer3fVisitor(manager) , xMin(0.0f), xMax(0.0f), yMin(0.0f), yMax(0.0f), zMin(0.0f), zMax(0.0f) { } float xMin, xMax, yMin, yMax, zMin, zMax; Vector3D xMinPt, xMaxPt, yMinPt, yMaxPt, zMinPt, zMaxPt; void visit(uint ndx, float x, float y, float z) override { if (ndx) { if (x < xMin) { xMin = x; xMinPt = Vector3D(x, y, z); } if (x > xMax) { xMax = x; xMaxPt = Vector3D(x, y, z); } if (y < yMin) { yMin = y; yMinPt = Vector3D(x, y, z); } if (y > yMax) { yMax = y; yMaxPt = Vector3D(x, y, z); } if (z < zMin) { zMin = z; zMinPt = Vector3D(x, y, z); } if (z > zMax) { zMax = z; zMaxPt = Vector3D(x, y, z); } } else { xMin = xMax = x; yMin = yMax = y; zMin = zMax = z; xMinPt = xMaxPt = yMinPt = yMaxPt = zMinPt = zMaxPt = Vector3D(x, y, z); } } }; class FindMaxDistantPoint : public Buffer3fVisitor { public: explicit FindMaxDistantPoint(NodeManagers *manager) : Buffer3fVisitor(manager) { } float maxLengthSquared = 0.0f; Vector3D maxDistPt; Vector3D referencePt; bool setReferencePoint = false; bool hasNoPoints = true; void visit(uint ndx, float x, float y, float z) override { Q_UNUSED(ndx); const Vector3D p = Vector3D(x, y, z); if (hasNoPoints && setReferencePoint) { maxLengthSquared = 0.0f; referencePt = p; } const float lengthSquared = (p - referencePt).lengthSquared(); if ( lengthSquared >= maxLengthSquared ) { maxDistPt = p; maxLengthSquared = lengthSquared; } hasNoPoints = false; } }; }; struct BoundingVolumeComputeData { Entity *entity = nullptr; GeometryRenderer *renderer = nullptr; Geometry *geometry = nullptr; Attribute *positionAttribute = nullptr; Attribute *indexAttribute = nullptr; int vertexCount = -1; bool valid() const { return vertexCount >= 0; } }; BoundingVolumeComputeData findBoundingVolumeComputeData(NodeManagers *manager, Entity *node) { BoundingVolumeComputeData res; res.entity = node; res.renderer = node->renderComponent(); if (!res.renderer || res.renderer->primitiveType() == QGeometryRenderer::Patches) return res; GeometryManager *geometryManager = manager->geometryManager(); res.geometry = geometryManager->lookupResource(res.renderer->geometryId()); if (!res.geometry) return res; // if it has a view, the bounding volume will have been computed by the core aspect if (res.renderer->hasView()) return res; int drawVertexCount = res.renderer->vertexCount(); // may be 0, gets changed below if so Qt3DRender::Render::Attribute *positionAttribute = manager->lookupResource(res.geometry->boundingPositionAttribute()); bool hasBoundingVolumePositionAttribute = positionAttribute != nullptr; // Use the default position attribute if attribute is null if (!hasBoundingVolumePositionAttribute) { const auto attrIds = res.geometry->attributes(); for (const Qt3DCore::QNodeId &attrId : attrIds) { positionAttribute = manager->lookupResource(attrId); if (positionAttribute && positionAttribute->name() == QAttribute::defaultPositionAttributeName()) break; } } if (!positionAttribute || positionAttribute->attributeType() != QAttribute::VertexAttribute || positionAttribute->vertexBaseType() != QAttribute::Float || positionAttribute->vertexSize() < 3) { qWarning("findBoundingVolumeComputeData: Position attribute not suited for bounding volume computation"); return res; } Buffer *buf = manager->lookupResource(positionAttribute->bufferId()); // No point in continuing if the positionAttribute doesn't have a suitable buffer if (!buf) { qWarning("findBoundingVolumeComputeData: Position attribute not referencing a valid buffer"); return res; } // Check if there is an index attribute. Qt3DRender::Render::Attribute *indexAttribute = nullptr; Buffer *indexBuf = nullptr; if (!hasBoundingVolumePositionAttribute) { const QList attributes = res.geometry->attributes(); for (Qt3DCore::QNodeId attrNodeId : attributes) { Qt3DRender::Render::Attribute *attr = manager->lookupResource(attrNodeId); if (attr && attr->attributeType() == QAttribute::IndexAttribute) { indexBuf = manager->lookupResource(attr->bufferId()); if (indexBuf) { indexAttribute = attr; if (!drawVertexCount) drawVertexCount = indexAttribute->count(); const QAttribute::VertexBaseType validIndexTypes[] = { QAttribute::UnsignedShort, QAttribute::UnsignedInt, QAttribute::UnsignedByte }; if (std::find(std::begin(validIndexTypes), std::end(validIndexTypes), indexAttribute->vertexBaseType()) == std::end(validIndexTypes)) { qWarning() << "findBoundingVolumeComputeData: Unsupported index attribute type" << indexAttribute->name() << indexAttribute->vertexBaseType(); return res; } break; } } } } if (hasBoundingVolumePositionAttribute || (!indexAttribute && !drawVertexCount)) drawVertexCount = positionAttribute->count(); // Buf will be set to not dirty once it's loaded // in a job executed after this one // We need to recompute the bounding volume // If anything in the GeometryRenderer has changed if (buf->isDirty() || node->isBoundingVolumeDirty() || positionAttribute->isDirty() || res.geometry->isDirty() || res.renderer->isDirty() || (indexAttribute && indexAttribute->isDirty()) || (indexBuf && indexBuf->isDirty())) { res.vertexCount = drawVertexCount; res.positionAttribute = positionAttribute; res.indexAttribute = indexAttribute; } return res; } std::vector calculateLocalBoundingVolume(NodeManagers *manager, const BoundingVolumeComputeData &data) { // The Bounding volume will only be computed if the position Buffer // isDirty std::vector updatedGeometries; BoundingVolumeCalculator reader(manager); if (reader.apply(data.positionAttribute, data.indexAttribute, data.vertexCount, data.renderer->primitiveRestartEnabled(), data.renderer->restartIndexValue())) { data.entity->localBoundingVolume()->setCenter(reader.result().center()); data.entity->localBoundingVolume()->setRadius(reader.result().radius()); data.entity->unsetBoundingVolumeDirty(); // Record min/max vertex in Geometry data.geometry->updateExtent(reader.min(), reader.max()); // Mark geometry as requiring a call to update its frontend updatedGeometries.push_back(data.geometry); } return updatedGeometries; } struct UpdateBoundFunctor { NodeManagers *manager; // This define is required to work with QtConcurrent typedef std::vector result_type; std::vector operator ()(const BoundingVolumeComputeData &data) { return calculateLocalBoundingVolume(manager, data); } }; struct ReduceUpdateBoundFunctor { void operator ()(std::vector &result, const std::vector &values) { result.insert(result.end(), values.begin(), values.end()); } }; class DirtyEntityAccumulator : public EntityVisitor { public: explicit DirtyEntityAccumulator(NodeManagers *manager) : EntityVisitor(manager) { } EntityVisitor::Operation visit(Entity *entity) override { if (!entity->isTreeEnabled()) return Prune; auto data = findBoundingVolumeComputeData(m_manager, entity); if (data.valid()) { // only valid if front end is a QGeometryRenderer without a view. All other cases handled by core aspect m_entities.push_back(data); } return Continue; } Qt3DCore::QAbstractFrontEndNodeManager *m_frontEndNodeManager = nullptr; std::vector m_entities; }; } // anonymous CalculateBoundingVolumeJob::CalculateBoundingVolumeJob() : Qt3DCore::QAspectJob() , m_manager(nullptr) , m_node(nullptr) , m_frontEndNodeManager(nullptr) { SET_JOB_RUN_STAT_TYPE(this, JobTypes::CalcBoundingVolume, 0) } void CalculateBoundingVolumeJob::run() { // There's 2 bounding volume jobs, one in Core, the other here in Render. // This one is setup to run after the other. // (see more details in Qt3DCore::CalculateBoundingVolumeJob::run) // // TODO: // - remove the one frame delay for propagating results of first job // - avoid copying the computed BV at every frame Q_ASSERT(m_frontEndNodeManager); DirtyEntityAccumulator accumulator(m_manager); accumulator.m_frontEndNodeManager = m_frontEndNodeManager; accumulator.apply(m_node); const std::vector entities = std::move(accumulator.m_entities); std::vector updatedGeometries; updatedGeometries.reserve(entities.size()); #if QT_CONFIG(concurrent) if (entities.size() > 1 && QAspectJobManager::idealThreadCount() > 1) { UpdateBoundFunctor functor; functor.manager = m_manager; ReduceUpdateBoundFunctor reduceFunctor; const std::vector &newGeometries = QtConcurrent::blockingMappedReduced(entities, functor, reduceFunctor); updatedGeometries.insert(updatedGeometries.end(), newGeometries.begin(), newGeometries.end()); } else #endif { for (const auto &data: entities) { const std::vector &newGeometries = calculateLocalBoundingVolume(m_manager, data); updatedGeometries.insert(updatedGeometries.end(), newGeometries.begin(), newGeometries.end()); } } m_updatedGeometries = std::move(updatedGeometries); } void CalculateBoundingVolumeJob::postFrame(QAspectEngine *aspectEngine) { Q_UNUSED(aspectEngine); for (Geometry *backend : m_updatedGeometries) { Qt3DCore::QGeometry *node = qobject_cast(m_frontEndNodeManager->lookupNode(backend->peerId())); if (!node) continue; Qt3DCore::QGeometryPrivate *dNode = static_cast(Qt3DCore::QNodePrivate::get(node)); dNode->setExtent(backend->min(), backend->max()); } m_updatedGeometries.clear(); } void CalculateBoundingVolumeJob::process(const Qt3DCore::BoundingVolumeComputeResult &result, bool computedResult) { // This gets called from the thread of the CalculateBoundingVolumeJob in the core aspect. // We receive the data calculated there and update our backend nodes auto entity = m_manager->renderNodesManager()->lookupResource(result.entity->id()); if (!entity) return; // copy data to the entity entity->localBoundingVolume()->setCenter(Vector3D(result.m_center)); entity->localBoundingVolume()->setRadius(std::max(result.m_radius, 0.0f)); entity->unsetBoundingVolumeDirty(); // copy the data to the geometry if (computedResult) { auto renderer = entity->renderComponent(); if (renderer) { auto geometry = m_manager->geometryManager()->lookupResource(renderer->geometryId()); if (geometry) geometry->updateExtent(result.m_min, result.m_max); } } } void CalculateBoundingVolumeJob::setRoot(Entity *node) { m_node = node; } void CalculateBoundingVolumeJob::setManagers(NodeManagers *manager) { m_manager = manager; } void CalculateBoundingVolumeJob::setFrontEndNodeManager(Qt3DCore::QAbstractFrontEndNodeManager *manager) { m_frontEndNodeManager = manager; } } // namespace Render } // namespace Qt3DRender QT_END_NAMESPACE