Revision control

++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include "qquick3druntimeloader_p.h"
++
++ #include <QtQuick3DAssetUtils/private/qssgscenedesc_p.h>
++ #include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
++ #include <QtQuick3DAssetUtils/private/qssgrtutilities_p.h>
++ #include <QtQuick3DAssetImport/private/qssgassetimportmanager_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
++ #if QT_CONFIG(mimetype)
++ #include <QtCore/qmimedatabase.h>
++ #endif
++
++ /*!
++ \qmltype RuntimeLoader
++ \inherits Node
++ \inqmlmodule QtQuick3D.AssetUtils
++ \since 6.2
++ \brief Imports a 3D asset at runtime.
++
++ The RuntimeLoader type provides a way to load a 3D asset directly from source at runtime,
++ without converting it to QtQuick3D's internal format first.
++
++ RuntimeLoader supports .obj and glTF version 2.0 files in both in text (.gltf) and binary
++ (.glb) formats.
++
++ glTF assets are loaded by the native glTF importer; see
++ \l{Balsam Asset Import Tool} for the supported feature set and
++ extensions. Setting the \c{QT_QUICK3D_DISABLE_NATIVE_GLTF} environment
++ variable routes glTF assets through the Assimp importer instead during
++ the transition period.
++
++ \warning RuntimeLoader does not sandbox or validate asset contents. Loading
++ malformed or untrusted assets may have security implications. See \l source
++ for details.
++ */
++
++ /*!
++ \qmlenum RuntimeLoader::QueryFilter
++
++ Specifies the type of objects to query for.
++
++ \value Textures Query for texture objects
++ \value Materials Query for material objects
++ \value Nodes Query for node objects
++ \value Cameras Query for camera objects
++ \value Lights Query for light objects
++ \value Models Query for model objects
++
++
++ \sa queryAll()
++ */
++
++ /*!
++ \qmlproperty url RuntimeLoader::source
++
++ This property holds the location of the source file containing the 3D asset.
++ Changing this property will unload the current asset and attempt to load an asset from
++ the given URL.
++
++ The success or failure of the load operation is indicated by \l status.
++
++ \warning RuntimeLoader does not sandbox or validate asset contents. Loading
++ malformed or untrusted assets may have security implications. Application
++ developers should carefully consider these before allowing the loading of
++ user-provided content that is not part of the application.
++ */
++
++ /*!
++ \qmlproperty enumeration RuntimeLoader::status
++
++ This property holds the status of the latest load operation.
++
++ \value RuntimeLoader.Empty
++ No URL was specified.
++ \value RuntimeLoader.Success
++ The load operation was successful.
++ \value RuntimeLoader.Error
++ The load operation failed. A human-readable error message is provided by \l errorString.
++
++ \readonly
++ */
++
++ /*!
++ \qmlproperty string RuntimeLoader::errorString
++
++ This property holds a human-readable string indicating the status of the latest load operation.
++
++ \readonly
++ */
++
++ /*!
++ \qmlproperty Bounds RuntimeLoader::bounds
++
++ This property describes the extents of the bounding volume around the imported model.
++
++ \note The value may not be available before the first render
++
++ \readonly
++ */
++
++ /*!
++ \qmlproperty Instancing RuntimeLoader::instancing
++
++ If this property is set, the imported model will not be rendered normally. Instead, a number of
++ instances will be rendered, as defined by the instance table.
++
++ See the \l{Instanced Rendering} overview documentation for more information.
++ */
++
++ QT_BEGIN_NAMESPACE
++
++ QQuick3DRuntimeLoader::QQuick3DRuntimeLoader(QQuick3DNode *parent)
++ : QQuick3DNode(parent)
++ {
++
++ }
++
++ QUrl QQuick3DRuntimeLoader::source() const
++ {
++ return m_source;
++ }
++
++ void QQuick3DRuntimeLoader::setSource(const QUrl &newSource)
++ {
++ if (m_source == newSource)
++ return;
++
++ const QQmlContext *context = qmlContext(this);
++ auto resolvedUrl = (context ? context->resolvedUrl(newSource) : newSource);
++
++ if (m_source == resolvedUrl)
++ return;
++
++ m_source = resolvedUrl;
++ emit sourceChanged();
++
++ if (isComponentComplete())
++ loadSource();
++ }
++
++ void QQuick3DRuntimeLoader::componentComplete()
++ {
++ QQuick3DNode::componentComplete();
++ loadSource();
++ }
++
++ QStringList QQuick3DRuntimeLoader::supportedExtensions()
++ {
++ static QStringList extensions;
++ if (!extensions.isEmpty())
++ return extensions;
++
++ static const QStringList supportedExtensions = { QLatin1StringView("obj"),
++ QLatin1StringView("gltf"),
++ QLatin1StringView("glb")};
++
++ QSSGAssetImportManager importManager;
++ const auto types = importManager.getImporterPluginInfos();
++
++ for (const auto &t : types) {
++ for (const QString &extension : t.inputExtensions) {
++ // Several plugins can claim the same extension
++ if (supportedExtensions.contains(extension) && !extensions.contains(extension))
++ extensions << extension;
++ }
++ }
++ return extensions;
++ }
++
++ #if QT_CONFIG(mimetype)
++ QList<QMimeType> QQuick3DRuntimeLoader::supportedMimeTypes()
++ {
++ static QList<QMimeType> mimeTypes;
++ if (!mimeTypes.isEmpty())
++ return mimeTypes;
++
++ const QStringList &extensions = supportedExtensions();
++
++ QMimeDatabase db;
++ for (const auto &ext : extensions) {
++ // TODO: Change to db.mimeTypesForExtension(ext), once it is implemented (QTBUG-118566)
++ const QString fileName = QLatin1StringView("test.") + ext;
++ mimeTypes << db.mimeTypesForFileName(fileName);
++ }
++
++ return mimeTypes;
++ }
++ #endif
++
++ static void boxBoundsRecursive(const QQuick3DNode *baseNode, const QQuick3DNode *node, QQuick3DBounds3 &accBounds)
++ {
++ if (!node)
++ return;
++
++ if (auto *model = qobject_cast<const QQuick3DModel *>(node)) {
++ auto b = model->bounds();
++ for (const QVector3D point : b.bounds.toQSSGBoxPoints()) {
++ auto p = model->mapPositionToNode(const_cast<QQuick3DNode *>(baseNode), point);
++ if (Q_UNLIKELY(accBounds.bounds.isEmpty()))
++ accBounds.bounds = { p, p };
++ else
++ accBounds.bounds.include(p);
++ }
++ }
++ const auto childItems1 = node->childItems();
++ for (auto *child : childItems1)
++ boxBoundsRecursive(baseNode, qobject_cast<const QQuick3DNode *>(child), accBounds);
++ }
++
++ template<typename Func>
++ static void applyToModels(QQuick3DObject *obj, Func &&lambda)
++ {
++ if (!obj)
++ return;
++ const auto childItems2 = obj->childItems();
++ for (auto *child : childItems2) {
++ if (auto *model = qobject_cast<QQuick3DModel *>(child))
++ lambda(model);
++ applyToModels(child, lambda);
++ }
++ }
++
++ void QQuick3DRuntimeLoader::loadSource()
++ {
++ delete m_root;
++ m_objects.clear();
++ m_objectsByType.clear();
++ const bool hadVariants = !m_materialVariants.isEmpty();
++ m_materialVariants.clear();
++ m_variantModels.clear();
++ if (hadVariants)
++ emit materialVariantsChanged();
++ QSSGBufferManager::unregisterMeshData(m_assetId);
++
++ m_status = Status::Empty;
++ m_errorString = QStringLiteral("No file selected");
++ if (!m_source.isValid()) {
++ emit statusChanged();
++ emit errorStringChanged();
++ return;
++ }
++
++ QSSGAssetImportManager importManager;
++ QSSGSceneDesc::Scene scene;
++ QString error(QStringLiteral("Unknown error"));
++ auto result = importManager.importFile(m_source, scene, &error);
++
++ switch (result) {
++ case QSSGAssetImportManager::ImportState::Success:
++ m_errorString = QStringLiteral("Success!");
++ m_status = Status::Success;
++ break;
++ case QSSGAssetImportManager::ImportState::IoError:
++ m_errorString = QStringLiteral("IO Error: ") + error;
++ m_status = Status::Error;
++ break;
++ case QSSGAssetImportManager::ImportState::Unsupported:
++ m_errorString = QStringLiteral("Unsupported: ") + error;
++ m_status = Status::Error;
++ break;
++ }
++
++ if (m_status == Status::Success) {
++ // We create a dummy root node here, as it will be the parent to the first-level nodes
++ // and resources. If we use 'this' those first-level nodes/resources won't be deleted
++ // when a new scene is loaded.
++ m_root = new QQuick3DNode(this);
++ m_root->setObjectName("RuntimeLoaderRoot");
++ m_imported = QSSGRuntimeUtils::createScene(*m_root, scene, &m_objects, &m_objectsByType);
++ m_assetId = scene.id;
++ m_boundsDirty = true;
++ m_instancingChanged = m_instancing != nullptr;
++
++ // Material variant tables have to be collected before cleanup()
++ m_materialVariants = scene.materialVariants;
++ m_variantModels.clear();
++ if (!m_materialVariants.isEmpty()) {
++ const auto materialObjects = [](const QList<QSSGSceneDesc::Node *> &materials) {
++ QList<QQuick3DMaterial *> result;
++ result.reserve(materials.size());
++ for (const QSSGSceneDesc::Node *material : materials)
++ result.append(qobject_cast<QQuick3DMaterial *>(material->obj));
++ return result;
++ };
++ QList<QSSGSceneDesc::Node *> pending { scene.root };
++ while (!pending.isEmpty()) {
++ QSSGSceneDesc::Node *node = pending.takeLast();
++ pending.append(node->children);
++ if (node->nodeType != QSSGSceneDesc::Node::Type::Model)
++ continue;
++ const auto &model = static_cast<const QSSGSceneDesc::Model &>(*node);
++ if (model.variantMaterials.isEmpty())
++ continue;
++ ModelVariantMaterials entry;
++ entry.model = qobject_cast<QQuick3DModel *>(model.obj);
++ entry.defaults = materialObjects(model.defaultMaterials);
++ for (const auto &variantList : model.variantMaterials)
++ entry.perVariant.append(materialObjects(variantList));
++ m_variantModels.append(entry);
++ }
++ }
++ emit materialVariantsChanged();
++
++ updateModels();
++ // Cleanup scene before deleting.
++ scene.cleanup();
++ } else {
++ m_source.clear();
++ emit sourceChanged();
++ }
++
++ emit statusChanged();
++ emit errorStringChanged();
++
++ }
++
++ void QQuick3DRuntimeLoader::updateModels()
++ {
++ if (m_instancingChanged) {
++ applyToModels(m_imported, [this](QQuick3DModel *model) {
++ model->setInstancing(m_instancing);
++ model->setInstanceRoot(m_imported);
++ });
++ m_instancingChanged = false;
++ }
++ if (!m_variantModels.isEmpty())
++ applyMaterialVariant();
++ }
++
++ void QQuick3DRuntimeLoader::applyMaterialVariant()
++ {
++ const qsizetype variantIndex = m_materialVariants.indexOf(m_materialVariant);
++ if (!m_materialVariant.isEmpty() && variantIndex < 0 && !m_materialVariants.isEmpty()) {
++ qWarning() << "Asset has no material variant" << m_materialVariant
++ << "- available variants:" << m_materialVariants;
++ }
++ for (const ModelVariantMaterials &entry : std::as_const(m_variantModels)) {
++ if (!entry.model)
++ continue;
++ const QList<QQuick3DMaterial *> &materials = (variantIndex >= 0 && variantIndex < entry.perVariant.size())
++ ? entry.perVariant.at(variantIndex)
++ : entry.defaults;
++ QQmlListProperty<QQuick3DMaterial> list = entry.model->materials();
++ list.clear(&list);
++ for (QQuick3DMaterial *material : materials)
++ list.append(&list, material);
++ }
++ }
++
++ /*!
++ \qmlproperty list<string> RuntimeLoader::materialVariants
++ \readonly
++ \since 6.13
++
++ This property holds the names of the material variants defined by the
++ loaded asset (for example glTF assets using the KHR_materials_variants
++ extension). The list is empty when the asset defines no variants.
++
++ \sa materialVariant
++ */
++ QStringList QQuick3DRuntimeLoader::materialVariants() const
++ {
++ return m_materialVariants;
++ }
++
++ /*!
++ \qmlproperty string RuntimeLoader::materialVariant
++ \since 6.13
++
++ This property selects which of the loaded asset's material variants is
++ applied to its models. Setting an empty string, or a name not present in
++ \l materialVariants, applies the asset's default materials.
++
++ \sa materialVariants
++ */
++ QString QQuick3DRuntimeLoader::materialVariant() const
++ {
++ return m_materialVariant;
++ }
++
++ void QQuick3DRuntimeLoader::setMaterialVariant(const QString &variant)
++ {
++ if (m_materialVariant == variant)
++ return;
++ m_materialVariant = variant;
++ if (!m_variantModels.isEmpty())
++ applyMaterialVariant();
++ emit materialVariantChanged();
++ }
++
++ QQuick3DRuntimeLoader::Status QQuick3DRuntimeLoader::status() const
++ {
++ return m_status;
++ }
++
++ QString QQuick3DRuntimeLoader::errorString() const
++ {
++ return m_errorString;
++ }
++
++ QSSGRenderGraphObject *QQuick3DRuntimeLoader::updateSpatialNode(QSSGRenderGraphObject *node)
++ {
++ auto *result = QQuick3DNode::updateSpatialNode(node);
++ if (m_boundsDirty)
++ QMetaObject::invokeMethod(this, &QQuick3DRuntimeLoader::boundsChanged, Qt::QueuedConnection);
++ return result;
++ }
++
++ void QQuick3DRuntimeLoader::calculateBounds()
++ {
++ if (!m_imported || !m_boundsDirty)
++ return;
++
++ m_bounds.bounds.setEmpty();
++ boxBoundsRecursive(m_imported, m_imported, m_bounds);
++ m_boundsDirty = false;
++ }
++
++ const QQuick3DBounds3 &QQuick3DRuntimeLoader::bounds() const
++ {
++ if (m_boundsDirty) {
++ auto *that = const_cast<QQuick3DRuntimeLoader *>(this);
++ that->calculateBounds();
++ return that->m_bounds;
++ }
++
++ return m_bounds;
++ }
++
++ QQuick3DInstancing *QQuick3DRuntimeLoader::instancing() const
++ {
++ return m_instancing;
++ }
++
++ void QQuick3DRuntimeLoader::setInstancing(QQuick3DInstancing *newInstancing)
++ {
++ if (m_instancing == newInstancing)
++ return;
++
++ QQuick3DObjectPrivate::attachWatcher(this, &QQuick3DRuntimeLoader::setInstancing,
++ newInstancing, m_instancing);
++
++ m_instancing = newInstancing;
++ m_instancingChanged = true;
++ updateModels();
++ emit instancingChanged();
++ }
++
++ /*!
++ \qmlmethod Object3D RuntimeLoader::query(string arg)
++ \since 6.12
++
++ Returns the object with the given name, or \c null if no object with that name exists.
++
++ The \a arg parameter is the name of the object to query or a query string.
++ For example, to query the object named "PaintMaterialX", use the following code:
++
++ \badcode
++ var object = runtimeLoader.query("PaintMaterial")
++ \endcode
++
++ The above code works as expected assuming the objects are sensibly named in the source asset file.
++ However, if there are multiple objects with the same name, the query will return the first object
++ found matching the given name. To query a specific object, and avoid ambiguity, use the full object path.
++
++ \badcode
++ var object = runtimeLoader.query("/House2/Wall001/Mirror/Material")
++ \endcode
++
++ \note Even with paths it's possible for a improperly structured asset file to have multiple objects with the same path,
++ as the paths are built up from the object names in the source asset file.
++
++ \note The object names are defined as in the source asset file.
++
++ \note A glTF node using \c EXT_mesh_gpu_instancing is imported as a Node
++ carrying the node's transform, with the instanced Model as its child. The
++ asset's name belongs to that Node, so querying such a node returns a Node
++ rather than a Model. Use queryAll() with the \c Models filter to reach the
++ Model itself.
++ */
++
++ QQuick3DObject *QQuick3DRuntimeLoader::query(const QString &name) const
++ {
++ const auto index = name.lastIndexOf(QChar(u'/'));
++
++ if (index != -1) {
++ const QString shortName = name.mid(index + 1);
++ const auto range = m_objects.equal_range({shortName, QString()});
++ for (auto it = range.first; it != range.second; ++it) {
++ if (it.key().path == name)
++ return it.value();
++ }
++ }
++
++ return m_objects.value(QSSGRuntimeObjectNameKey{name, QString()});
++ }
++
++ static inline QSSGRenderGraphObject::BaseType queryFilterToBaseType(QQuick3DRuntimeLoader::QueryFilter filter)
++ {
++ using Type = QSSGRenderGraphObject::Type;
++ switch (filter) {
++ case QQuick3DRuntimeLoader::QueryFilter::Textures:
++ return QSSGRenderGraphObjectUtils::getBaseType(Type::Image2D);
++ case QQuick3DRuntimeLoader::QueryFilter::Materials:
++ return QSSGRenderGraphObjectUtils::getBaseType(Type::PrincipledMaterial);
++ case QQuick3DRuntimeLoader::QueryFilter::Nodes:
++ return QSSGRenderGraphObjectUtils::getBaseType(Type::Node);
++ case QQuick3DRuntimeLoader::QueryFilter::Cameras:
++ return QSSGRenderGraphObjectUtils::getBaseType(Type::OrthographicCamera);
++ case QQuick3DRuntimeLoader::QueryFilter::Lights:
++ return QSSGRenderGraphObjectUtils::getBaseType(Type::DirectionalLight);
++ case QQuick3DRuntimeLoader::QueryFilter::Models:
++ return QSSGRenderGraphObjectUtils::getBaseType(Type::Model);
++ }
++
++ Q_UNREACHABLE_RETURN(QSSGRenderGraphObject::BaseType(0));
++ }
++
++ /*!
++ \qmlmethod List<Object3D> RuntimeLoader::queryAll(QueryFilter filter)
++ \since 6.12
++
++ Returns a list of all objects matching the given filter.
++
++ The \a filter parameter specifies the type of objects to query for.
++ For example, to query for all materials, use the following code:
++
++ \badcode
++ var materials = runtimeLoader.queryAll(RuntimeLoader.Materials)
++ \endcode
++
++ The above code returns a list of all materials in the source asset file.
++ */
++
++ QList<QQuick3DObject *> QQuick3DRuntimeLoader::queryAll(QueryFilter filter) const
++ {
++ QList<QQuick3DObject *> results;
++ const auto range = m_objectsByType.equal_range(queryFilterToBaseType(filter));
++ for (auto it = range.first; it != range.second; ++it) {
++ if (auto *obj = it.value().data())
++ results << obj;
++ }
++ return results;
++ }
++
++ QT_END_NAMESPACE