Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquick3druntimeloader.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5
7
8#include <QtQuick3DAssetUtils/private/qssgscenedesc_p.h>
9#include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
10#include <QtQuick3DAssetUtils/private/qssgrtutilities_p.h>
11#include <QtQuick3DAssetImport/private/qssgassetimportmanager_p.h>
12#include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
13#if QT_CONFIG(mimetype)
14#include <QtCore/qmimedatabase.h>
15#endif
16
17/*!
18 \qmltype RuntimeLoader
19 \inherits Node
20 \inqmlmodule QtQuick3D.AssetUtils
21 \since 6.2
22 \brief Imports a 3D asset at runtime.
23
24 The RuntimeLoader type provides a way to load a 3D asset directly from source at runtime,
25 without converting it to QtQuick3D's internal format first.
26
27 RuntimeLoader supports .obj and glTF version 2.0 files in both in text (.gltf) and binary
28 (.glb) formats.
29
30 glTF assets are loaded by the native glTF importer; see
31 \l{Balsam Asset Import Tool} for the supported feature set and
32 extensions. Setting the \c{QT_QUICK3D_DISABLE_NATIVE_GLTF} environment
33 variable routes glTF assets through the Assimp importer instead during
34 the transition period.
35
36 \warning RuntimeLoader does not sandbox or validate asset contents. Loading
37 malformed or untrusted assets may have security implications. See \l source
38 for details.
39*/
40
41/*!
42 \qmlenum RuntimeLoader::QueryFilter
43
44 Specifies the type of objects to query for.
45
46 \value Textures Query for texture objects
47 \value Materials Query for material objects
48 \value Nodes Query for node objects
49 \value Cameras Query for camera objects
50 \value Lights Query for light objects
51 \value Models Query for model objects
52
53
54 \sa queryAll()
55*/
56
57/*!
58 \qmlproperty url RuntimeLoader::source
59
60 This property holds the location of the source file containing the 3D asset.
61 Changing this property will unload the current asset and attempt to load an asset from
62 the given URL.
63
64 The success or failure of the load operation is indicated by \l status.
65
66 \warning RuntimeLoader does not sandbox or validate asset contents. Loading
67 malformed or untrusted assets may have security implications. Application
68 developers should carefully consider these before allowing the loading of
69 user-provided content that is not part of the application.
70*/
71
72/*!
73 \qmlproperty enumeration RuntimeLoader::status
74
75 This property holds the status of the latest load operation.
76
77 \value RuntimeLoader.Empty
78 No URL was specified.
79 \value RuntimeLoader.Success
80 The load operation was successful.
81 \value RuntimeLoader.Error
82 The load operation failed. A human-readable error message is provided by \l errorString.
83
84 \readonly
85*/
86
87/*!
88 \qmlproperty string RuntimeLoader::errorString
89
90 This property holds a human-readable string indicating the status of the latest load operation.
91
92 \readonly
93*/
94
95/*!
96 \qmlproperty Bounds RuntimeLoader::bounds
97
98 This property describes the extents of the bounding volume around the imported model.
99
100 \note The value may not be available before the first render
101
102 \readonly
103*/
104
105/*!
106 \qmlproperty Instancing RuntimeLoader::instancing
107
108 If this property is set, the imported model will not be rendered normally. Instead, a number of
109 instances will be rendered, as defined by the instance table.
110
111 See the \l{Instanced Rendering} overview documentation for more information.
112*/
113
115
116QQuick3DRuntimeLoader::QQuick3DRuntimeLoader(QQuick3DNode *parent)
117 : QQuick3DNode(parent)
118{
119
120}
121
122QUrl QQuick3DRuntimeLoader::source() const
123{
124 return m_source;
125}
126
127void QQuick3DRuntimeLoader::setSource(const QUrl &newSource)
128{
129 if (m_source == newSource)
130 return;
131
132 const QQmlContext *context = qmlContext(this);
133 auto resolvedUrl = (context ? context->resolvedUrl(newSource) : newSource);
134
135 if (m_source == resolvedUrl)
136 return;
137
138 m_source = resolvedUrl;
139 emit sourceChanged();
140
141 if (isComponentComplete())
142 loadSource();
143}
144
145void QQuick3DRuntimeLoader::componentComplete()
146{
147 QQuick3DNode::componentComplete();
148 loadSource();
149}
150
151QStringList QQuick3DRuntimeLoader::supportedExtensions()
152{
153 static QStringList extensions;
154 if (!extensions.isEmpty())
155 return extensions;
156
157 static const QStringList supportedExtensions = { QLatin1StringView("obj"),
158 QLatin1StringView("gltf"),
159 QLatin1StringView("glb")};
160
161 QSSGAssetImportManager importManager;
162 const auto types = importManager.getImporterPluginInfos();
163
164 for (const auto &t : types) {
165 for (const QString &extension : t.inputExtensions) {
166 // Several plugins can claim the same extension
167 if (supportedExtensions.contains(extension) && !extensions.contains(extension))
168 extensions << extension;
169 }
170 }
171 return extensions;
172}
173
174#if QT_CONFIG(mimetype)
175QList<QMimeType> QQuick3DRuntimeLoader::supportedMimeTypes()
176{
177 static QList<QMimeType> mimeTypes;
178 if (!mimeTypes.isEmpty())
179 return mimeTypes;
180
181 const QStringList &extensions = supportedExtensions();
182
183 QMimeDatabase db;
184 for (const auto &ext : extensions) {
185 // TODO: Change to db.mimeTypesForExtension(ext), once it is implemented (QTBUG-118566)
186 const QString fileName = QLatin1StringView("test.") + ext;
187 mimeTypes << db.mimeTypesForFileName(fileName);
188 }
189
190 return mimeTypes;
191}
192#endif
193
194static void boxBoundsRecursive(const QQuick3DNode *baseNode, const QQuick3DNode *node, QQuick3DBounds3 &accBounds)
195{
196 if (!node)
197 return;
198
199 if (auto *model = qobject_cast<const QQuick3DModel *>(node)) {
200 auto b = model->bounds();
201 for (const QVector3D point : b.bounds.toQSSGBoxPoints()) {
202 auto p = model->mapPositionToNode(const_cast<QQuick3DNode *>(baseNode), point);
203 if (Q_UNLIKELY(accBounds.bounds.isEmpty()))
204 accBounds.bounds = { p, p };
205 else
206 accBounds.bounds.include(p);
207 }
208 }
209 const auto childItems1 = node->childItems();
210 for (auto *child : childItems1)
211 boxBoundsRecursive(baseNode, qobject_cast<const QQuick3DNode *>(child), accBounds);
212}
213
214template<typename Func>
215static void applyToModels(QQuick3DObject *obj, Func &&lambda)
216{
217 if (!obj)
218 return;
219 const auto childItems2 = obj->childItems();
220 for (auto *child : childItems2) {
221 if (auto *model = qobject_cast<QQuick3DModel *>(child))
222 lambda(model);
223 applyToModels(child, lambda);
224 }
225}
226
227void QQuick3DRuntimeLoader::loadSource()
228{
229 delete m_root;
230 m_objects.clear();
231 m_objectsByType.clear();
232 const bool hadVariants = !m_materialVariants.isEmpty();
233 m_materialVariants.clear();
234 m_variantModels.clear();
235 if (hadVariants)
236 emit materialVariantsChanged();
237 QSSGBufferManager::unregisterMeshData(m_assetId);
238
239 m_status = Status::Empty;
240 m_errorString = QStringLiteral("No file selected");
241 if (!m_source.isValid()) {
242 emit statusChanged();
243 emit errorStringChanged();
244 return;
245 }
246
247 QSSGAssetImportManager importManager;
248 QSSGSceneDesc::Scene scene;
249 QString error(QStringLiteral("Unknown error"));
250 auto result = importManager.importFile(m_source, scene, &error);
251
252 switch (result) {
253 case QSSGAssetImportManager::ImportState::Success:
254 m_errorString = QStringLiteral("Success!");
255 m_status = Status::Success;
256 break;
257 case QSSGAssetImportManager::ImportState::IoError:
258 m_errorString = QStringLiteral("IO Error: ") + error;
259 m_status = Status::Error;
260 break;
261 case QSSGAssetImportManager::ImportState::Unsupported:
262 m_errorString = QStringLiteral("Unsupported: ") + error;
263 m_status = Status::Error;
264 break;
265 }
266
267 if (m_status == Status::Success) {
268 // We create a dummy root node here, as it will be the parent to the first-level nodes
269 // and resources. If we use 'this' those first-level nodes/resources won't be deleted
270 // when a new scene is loaded.
271 m_root = new QQuick3DNode(this);
272 m_root->setObjectName("RuntimeLoaderRoot");
273 m_imported = QSSGRuntimeUtils::createScene(*m_root, scene, &m_objects, &m_objectsByType);
274 m_assetId = scene.id;
275 m_boundsDirty = true;
276 m_instancingChanged = m_instancing != nullptr;
277
278 // Material variant tables have to be collected before cleanup()
279 m_materialVariants = scene.materialVariants;
280 m_variantModels.clear();
281 if (!m_materialVariants.isEmpty()) {
282 const auto materialObjects = [](const QList<QSSGSceneDesc::Node *> &materials) {
283 QList<QQuick3DMaterial *> result;
284 result.reserve(materials.size());
285 for (const QSSGSceneDesc::Node *material : materials)
286 result.append(qobject_cast<QQuick3DMaterial *>(material->obj));
287 return result;
288 };
289 QList<QSSGSceneDesc::Node *> pending { scene.root };
290 while (!pending.isEmpty()) {
291 QSSGSceneDesc::Node *node = pending.takeLast();
292 pending.append(node->children);
293 if (node->nodeType != QSSGSceneDesc::Node::Type::Model)
294 continue;
295 const auto &model = static_cast<const QSSGSceneDesc::Model &>(*node);
296 if (model.variantMaterials.isEmpty())
297 continue;
298 ModelVariantMaterials entry;
299 entry.model = qobject_cast<QQuick3DModel *>(model.obj);
300 entry.defaults = materialObjects(model.defaultMaterials);
301 for (const auto &variantList : model.variantMaterials)
302 entry.perVariant.append(materialObjects(variantList));
303 m_variantModels.append(entry);
304 }
305 }
306 emit materialVariantsChanged();
307
308 updateModels();
309 // Cleanup scene before deleting.
310 scene.cleanup();
311 } else {
312 m_source.clear();
313 emit sourceChanged();
314 }
315
316 emit statusChanged();
317 emit errorStringChanged();
318
319}
320
321void QQuick3DRuntimeLoader::updateModels()
322{
323 if (m_instancingChanged) {
324 applyToModels(m_imported, [this](QQuick3DModel *model) {
325 model->setInstancing(m_instancing);
326 model->setInstanceRoot(m_imported);
327 });
328 m_instancingChanged = false;
329 }
330 if (!m_variantModels.isEmpty())
331 applyMaterialVariant();
332}
333
334void QQuick3DRuntimeLoader::applyMaterialVariant()
335{
336 const qsizetype variantIndex = m_materialVariants.indexOf(m_materialVariant);
337 if (!m_materialVariant.isEmpty() && variantIndex < 0 && !m_materialVariants.isEmpty()) {
338 qWarning() << "Asset has no material variant" << m_materialVariant
339 << "- available variants:" << m_materialVariants;
340 }
341 for (const ModelVariantMaterials &entry : std::as_const(m_variantModels)) {
342 if (!entry.model)
343 continue;
344 const QList<QQuick3DMaterial *> &materials = (variantIndex >= 0 && variantIndex < entry.perVariant.size())
345 ? entry.perVariant.at(variantIndex)
346 : entry.defaults;
347 QQmlListProperty<QQuick3DMaterial> list = entry.model->materials();
348 list.clear(&list);
349 for (QQuick3DMaterial *material : materials)
350 list.append(&list, material);
351 }
352}
353
354/*!
355 \qmlproperty list<string> RuntimeLoader::materialVariants
356 \readonly
357 \since 6.13
358
359 This property holds the names of the material variants defined by the
360 loaded asset (for example glTF assets using the KHR_materials_variants
361 extension). The list is empty when the asset defines no variants.
362
363 \sa materialVariant
364*/
365QStringList QQuick3DRuntimeLoader::materialVariants() const
366{
367 return m_materialVariants;
368}
369
370/*!
371 \qmlproperty string RuntimeLoader::materialVariant
372 \since 6.13
373
374 This property selects which of the loaded asset's material variants is
375 applied to its models. Setting an empty string, or a name not present in
376 \l materialVariants, applies the asset's default materials.
377
378 \sa materialVariants
379*/
380QString QQuick3DRuntimeLoader::materialVariant() const
381{
382 return m_materialVariant;
383}
384
385void QQuick3DRuntimeLoader::setMaterialVariant(const QString &variant)
386{
387 if (m_materialVariant == variant)
388 return;
389 m_materialVariant = variant;
390 if (!m_variantModels.isEmpty())
391 applyMaterialVariant();
392 emit materialVariantChanged();
393}
394
395QQuick3DRuntimeLoader::Status QQuick3DRuntimeLoader::status() const
396{
397 return m_status;
398}
399
400QString QQuick3DRuntimeLoader::errorString() const
401{
402 return m_errorString;
403}
404
405QSSGRenderGraphObject *QQuick3DRuntimeLoader::updateSpatialNode(QSSGRenderGraphObject *node)
406{
407 auto *result = QQuick3DNode::updateSpatialNode(node);
408 if (m_boundsDirty)
409 QMetaObject::invokeMethod(this, &QQuick3DRuntimeLoader::boundsChanged, Qt::QueuedConnection);
410 return result;
411}
412
413void QQuick3DRuntimeLoader::calculateBounds()
414{
415 if (!m_imported || !m_boundsDirty)
416 return;
417
418 m_bounds.bounds.setEmpty();
419 boxBoundsRecursive(m_imported, m_imported, m_bounds);
420 m_boundsDirty = false;
421}
422
423const QQuick3DBounds3 &QQuick3DRuntimeLoader::bounds() const
424{
425 if (m_boundsDirty) {
426 auto *that = const_cast<QQuick3DRuntimeLoader *>(this);
427 that->calculateBounds();
428 return that->m_bounds;
429 }
430
431 return m_bounds;
432}
433
434QQuick3DInstancing *QQuick3DRuntimeLoader::instancing() const
435{
436 return m_instancing;
437}
438
439void QQuick3DRuntimeLoader::setInstancing(QQuick3DInstancing *newInstancing)
440{
441 if (m_instancing == newInstancing)
442 return;
443
444 QQuick3DObjectPrivate::attachWatcher(this, &QQuick3DRuntimeLoader::setInstancing,
445 newInstancing, m_instancing);
446
447 m_instancing = newInstancing;
448 m_instancingChanged = true;
449 updateModels();
450 emit instancingChanged();
451}
452
453/*!
454 \qmlmethod Object3D RuntimeLoader::query(string arg)
455 \since 6.12
456
457 Returns the object with the given name, or \c null if no object with that name exists.
458
459 The \a arg parameter is the name of the object to query or a query string.
460 For example, to query the object named "PaintMaterialX", use the following code:
461
462 \badcode
463 var object = runtimeLoader.query("PaintMaterial")
464 \endcode
465
466 The above code works as expected assuming the objects are sensibly named in the source asset file.
467 However, if there are multiple objects with the same name, the query will return the first object
468 found matching the given name. To query a specific object, and avoid ambiguity, use the full object path.
469
470 \badcode
471 var object = runtimeLoader.query("/House2/Wall001/Mirror/Material")
472 \endcode
473
474 \note Even with paths it's possible for a improperly structured asset file to have multiple objects with the same path,
475 as the paths are built up from the object names in the source asset file.
476
477 \note The object names are defined as in the source asset file.
478
479 \note A glTF node using \c EXT_mesh_gpu_instancing is imported as a Node
480 carrying the node's transform, with the instanced Model as its child. The
481 asset's name belongs to that Node, so querying such a node returns a Node
482 rather than a Model. Use queryAll() with the \c Models filter to reach the
483 Model itself.
484*/
485
486QQuick3DObject *QQuick3DRuntimeLoader::query(const QString &name) const
487{
488 const auto index = name.lastIndexOf(QChar(u'/'));
489
490 if (index != -1) {
491 const QString shortName = name.mid(index + 1);
492 const auto range = m_objects.equal_range({shortName, QString()});
493 for (auto it = range.first; it != range.second; ++it) {
494 if (it.key().path == name)
495 return it.value();
496 }
497 }
498
499 return m_objects.value(QSSGRuntimeObjectNameKey{name, QString()});
500}
501
502static inline QSSGRenderGraphObject::BaseType queryFilterToBaseType(QQuick3DRuntimeLoader::QueryFilter filter)
503{
504 using Type = QSSGRenderGraphObject::Type;
505 switch (filter) {
506 case QQuick3DRuntimeLoader::QueryFilter::Textures:
507 return QSSGRenderGraphObjectUtils::getBaseType(Type::Image2D);
508 case QQuick3DRuntimeLoader::QueryFilter::Materials:
509 return QSSGRenderGraphObjectUtils::getBaseType(Type::PrincipledMaterial);
510 case QQuick3DRuntimeLoader::QueryFilter::Nodes:
511 return QSSGRenderGraphObjectUtils::getBaseType(Type::Node);
512 case QQuick3DRuntimeLoader::QueryFilter::Cameras:
513 return QSSGRenderGraphObjectUtils::getBaseType(Type::OrthographicCamera);
514 case QQuick3DRuntimeLoader::QueryFilter::Lights:
515 return QSSGRenderGraphObjectUtils::getBaseType(Type::DirectionalLight);
516 case QQuick3DRuntimeLoader::QueryFilter::Models:
517 return QSSGRenderGraphObjectUtils::getBaseType(Type::Model);
518 }
519
520 Q_UNREACHABLE_RETURN(QSSGRenderGraphObject::BaseType(0));
521}
522
523/*!
524 \qmlmethod List<Object3D> RuntimeLoader::queryAll(QueryFilter filter)
525 \since 6.12
526
527 Returns a list of all objects matching the given filter.
528
529 The \a filter parameter specifies the type of objects to query for.
530 For example, to query for all materials, use the following code:
531
532 \badcode
533 var materials = runtimeLoader.queryAll(RuntimeLoader.Materials)
534 \endcode
535
536 The above code returns a list of all materials in the source asset file.
537*/
538
539QList<QQuick3DObject *> QQuick3DRuntimeLoader::queryAll(QueryFilter filter) const
540{
541 QList<QQuick3DObject *> results;
542 const auto range = m_objectsByType.equal_range(queryFilterToBaseType(filter));
543 for (auto it = range.first; it != range.second; ++it) {
544 if (auto *obj = it.value().data())
545 results << obj;
546 }
547 return results;
548}
549
550QT_END_NAMESPACE
Combined button and popup list for selecting options.
static void boxBoundsRecursive(const QQuick3DNode *baseNode, const QQuick3DNode *node, QQuick3DBounds3 &accBounds)
static void applyToModels(QQuick3DObject *obj, Func &&lambda)
static QSSGRenderGraphObject::BaseType queryFilterToBaseType(QQuick3DRuntimeLoader::QueryFilter filter)