Revision control
Copy as Markdown
// 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