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 \warning RuntimeLoader does not sandbox or validate asset contents. Loading
31 malformed or untrusted assets may have security implications. See \l source
32 for details.
33*/
34
35/*!
36 \qmlproperty url RuntimeLoader::source
37
38 This property holds the location of the source file containing the 3D asset.
39 Changing this property will unload the current asset and attempt to load an asset from
40 the given URL.
41
42 The success or failure of the load operation is indicated by \l status.
43
44 \warning RuntimeLoader does not sandbox or validate asset contents. Loading
45 malformed or untrusted assets may have security implications. Application
46 developers should carefully consider these before allowing the loading of
47 user-provided content that is not part of the application.
48*/
49
50/*!
51 \qmlproperty enumeration RuntimeLoader::status
52
53 This property holds the status of the latest load operation.
54
55 \value RuntimeLoader.Empty
56 No URL was specified.
57 \value RuntimeLoader.Success
58 The load operation was successful.
59 \value RuntimeLoader.Error
60 The load operation failed. A human-readable error message is provided by \l errorString.
61
62 \readonly
63*/
64
65/*!
66 \qmlproperty string RuntimeLoader::errorString
67
68 This property holds a human-readable string indicating the status of the latest load operation.
69
70 \readonly
71*/
72
73/*!
74 \qmlproperty Bounds RuntimeLoader::bounds
75
76 This property describes the extents of the bounding volume around the imported model.
77
78 \note The value may not be available before the first render
79
80 \readonly
81*/
82
83/*!
84 \qmlproperty Instancing RuntimeLoader::instancing
85
86 If this property is set, the imported model will not be rendered normally. Instead, a number of
87 instances will be rendered, as defined by the instance table.
88
89 See the \l{Instanced Rendering} overview documentation for more information.
90*/
91
93
94QQuick3DRuntimeLoader::QQuick3DRuntimeLoader(QQuick3DNode *parent)
95 : QQuick3DNode(parent)
96{
97
98}
99
100QUrl QQuick3DRuntimeLoader::source() const
101{
102 return m_source;
103}
104
105void QQuick3DRuntimeLoader::setSource(const QUrl &newSource)
106{
107 if (m_source == newSource)
108 return;
109
110 const QQmlContext *context = qmlContext(this);
111 auto resolvedUrl = (context ? context->resolvedUrl(newSource) : newSource);
112
113 if (m_source == resolvedUrl)
114 return;
115
116 m_source = resolvedUrl;
117 emit sourceChanged();
118
119 if (isComponentComplete())
120 loadSource();
121}
122
123void QQuick3DRuntimeLoader::componentComplete()
124{
125 QQuick3DNode::componentComplete();
126 loadSource();
127}
128
129QStringList QQuick3DRuntimeLoader::supportedExtensions()
130{
131 static QStringList extensions;
132 if (!extensions.isEmpty())
133 return extensions;
134
135 static const QStringList supportedExtensions = { QLatin1StringView("obj"),
136 QLatin1StringView("gltf"),
137 QLatin1StringView("glb")};
138
139 QSSGAssetImportManager importManager;
140 const auto types = importManager.getImporterPluginInfos();
141
142 for (const auto &t : types) {
143 for (const QString &extension : t.inputExtensions) {
144 if (supportedExtensions.contains(extension))
145 extensions << extension;
146 }
147 }
148 return extensions;
149}
150
151#if QT_CONFIG(mimetype)
152QList<QMimeType> QQuick3DRuntimeLoader::supportedMimeTypes()
153{
154 static QList<QMimeType> mimeTypes;
155 if (!mimeTypes.isEmpty())
156 return mimeTypes;
157
158 const QStringList &extensions = supportedExtensions();
159
160 QMimeDatabase db;
161 for (const auto &ext : extensions) {
162 // TODO: Change to db.mimeTypesForExtension(ext), once it is implemented (QTBUG-118566)
163 const QString fileName = QLatin1StringView("test.") + ext;
164 mimeTypes << db.mimeTypesForFileName(fileName);
165 }
166
167 return mimeTypes;
168}
169#endif
170
171static void boxBoundsRecursive(const QQuick3DNode *baseNode, const QQuick3DNode *node, QQuick3DBounds3 &accBounds)
172{
173 if (!node)
174 return;
175
176 if (auto *model = qobject_cast<const QQuick3DModel *>(node)) {
177 auto b = model->bounds();
178 for (const QVector3D point : b.bounds.toQSSGBoxPoints()) {
179 auto p = model->mapPositionToNode(const_cast<QQuick3DNode *>(baseNode), point);
180 if (Q_UNLIKELY(accBounds.bounds.isEmpty()))
181 accBounds.bounds = { p, p };
182 else
183 accBounds.bounds.include(p);
184 }
185 }
186 const auto childItems1 = node->childItems();
187 for (auto *child : childItems1)
188 boxBoundsRecursive(baseNode, qobject_cast<const QQuick3DNode *>(child), accBounds);
189}
190
191template<typename Func>
192static void applyToModels(QQuick3DObject *obj, Func &&lambda)
193{
194 if (!obj)
195 return;
196 const auto childItems2 = obj->childItems();
197 for (auto *child : childItems2) {
198 if (auto *model = qobject_cast<QQuick3DModel *>(child))
199 lambda(model);
200 applyToModels(child, lambda);
201 }
202}
203
204void QQuick3DRuntimeLoader::loadSource()
205{
206 delete m_root;
207 m_objects.clear();
208 m_objectsByType.clear();
209 QSSGBufferManager::unregisterMeshData(m_assetId);
210
211 m_status = Status::Empty;
212 m_errorString = QStringLiteral("No file selected");
213 if (!m_source.isValid()) {
214 emit statusChanged();
215 emit errorStringChanged();
216 return;
217 }
218
219 QSSGAssetImportManager importManager;
220 QSSGSceneDesc::Scene scene;
221 QString error(QStringLiteral("Unknown error"));
222 auto result = importManager.importFile(m_source, scene, &error);
223
224 switch (result) {
225 case QSSGAssetImportManager::ImportState::Success:
226 m_errorString = QStringLiteral("Success!");
227 m_status = Status::Success;
228 break;
229 case QSSGAssetImportManager::ImportState::IoError:
230 m_errorString = QStringLiteral("IO Error: ") + error;
231 m_status = Status::Error;
232 break;
233 case QSSGAssetImportManager::ImportState::Unsupported:
234 m_errorString = QStringLiteral("Unsupported: ") + error;
235 m_status = Status::Error;
236 break;
237 }
238
239 if (m_status == Status::Success) {
240 // We create a dummy root node here, as it will be the parent to the first-level nodes
241 // and resources. If we use 'this' those first-level nodes/resources won't be deleted
242 // when a new scene is loaded.
243 m_root = new QQuick3DNode(this);
244 m_root->setObjectName("RuntimeLoaderRoot");
245 m_imported = QSSGRuntimeUtils::createScene(*m_root, scene, &m_objects, &m_objectsByType);
246 m_assetId = scene.id;
247 m_boundsDirty = true;
248 m_instancingChanged = m_instancing != nullptr;
249 updateModels();
250 // Cleanup scene before deleting.
251 scene.cleanup();
252 } else {
253 m_source.clear();
254 emit sourceChanged();
255 }
256
257 emit statusChanged();
258 emit errorStringChanged();
259
260}
261
262void QQuick3DRuntimeLoader::updateModels()
263{
264 if (m_instancingChanged) {
265 applyToModels(m_imported, [this](QQuick3DModel *model) {
266 model->setInstancing(m_instancing);
267 model->setInstanceRoot(m_imported);
268 });
269 m_instancingChanged = false;
270 }
271}
272
273QQuick3DRuntimeLoader::Status QQuick3DRuntimeLoader::status() const
274{
275 return m_status;
276}
277
278QString QQuick3DRuntimeLoader::errorString() const
279{
280 return m_errorString;
281}
282
283QSSGRenderGraphObject *QQuick3DRuntimeLoader::updateSpatialNode(QSSGRenderGraphObject *node)
284{
285 auto *result = QQuick3DNode::updateSpatialNode(node);
286 if (m_boundsDirty)
287 QMetaObject::invokeMethod(this, &QQuick3DRuntimeLoader::boundsChanged, Qt::QueuedConnection);
288 return result;
289}
290
291void QQuick3DRuntimeLoader::calculateBounds()
292{
293 if (!m_imported || !m_boundsDirty)
294 return;
295
296 m_bounds.bounds.setEmpty();
297 boxBoundsRecursive(m_imported, m_imported, m_bounds);
298 m_boundsDirty = false;
299}
300
301const QQuick3DBounds3 &QQuick3DRuntimeLoader::bounds() const
302{
303 if (m_boundsDirty) {
304 auto *that = const_cast<QQuick3DRuntimeLoader *>(this);
305 that->calculateBounds();
306 return that->m_bounds;
307 }
308
309 return m_bounds;
310}
311
312QQuick3DInstancing *QQuick3DRuntimeLoader::instancing() const
313{
314 return m_instancing;
315}
316
317void QQuick3DRuntimeLoader::setInstancing(QQuick3DInstancing *newInstancing)
318{
319 if (m_instancing == newInstancing)
320 return;
321
322 QQuick3DObjectPrivate::attachWatcher(this, &QQuick3DRuntimeLoader::setInstancing,
323 newInstancing, m_instancing);
324
325 m_instancing = newInstancing;
326 m_instancingChanged = true;
327 updateModels();
328 emit instancingChanged();
329}
330
331/*!
332 \qmlmethod Object3D RuntimeLoader::query(string arg)
333 \since 6.12
334
335 Returns the object with the given name, or \c null if no object with that name exists.
336
337 The \a arg parameter is the name of the object to query or a query string.
338 For example, to query the object named "PaintMaterialX", use the following code:
339
340 \badcode
341 var object = runtimeLoader.query("PaintMaterial")
342 \endcode
343
344 The above code works as expected assuming the objects are sensibly named in the source asset file.
345 However, if there are multiple objects with the same name, the query will return the first object
346 found matching the given name. To query a specific object, and avoid ambiguity, use the full object path.
347
348 \badcode
349 var object = runtimeLoader.query("/House2/Wall001/Mirror/Material")
350 \endcode
351
352 \note Even with paths it's possible for a improperly structured asset file to have multiple objects with the same path,
353 as the paths are built up from the object names in the source asset file.
354
355 \note The object names are defined as in the source asset file.
356*/
357
358QQuick3DObject *QQuick3DRuntimeLoader::query(const QString &name) const
359{
360 const auto index = name.lastIndexOf(QChar(u'/'));
361
362 if (index != -1) {
363 const QString shortName = name.mid(index + 1);
364 const auto range = m_objects.equal_range({shortName, QString()});
365 for (auto it = range.first; it != range.second; ++it) {
366 if (it.key().path == name)
367 return it.value();
368 }
369 }
370
371 return m_objects.value(QSSGRuntimeObjectNameKey{name, QString()});
372}
373
374static inline QSSGRenderGraphObject::BaseType queryFilterToBaseType(QQuick3DRuntimeLoader::QueryFilter filter)
375{
376 using Type = QSSGRenderGraphObject::Type;
377 switch (filter) {
378 case QQuick3DRuntimeLoader::QueryFilter::Textures:
379 return QSSGRenderGraphObjectUtils::getBaseType(Type::Image2D);
380 case QQuick3DRuntimeLoader::QueryFilter::Materials:
381 return QSSGRenderGraphObjectUtils::getBaseType(Type::PrincipledMaterial);
382 case QQuick3DRuntimeLoader::QueryFilter::Nodes:
383 return QSSGRenderGraphObjectUtils::getBaseType(Type::Node);
384 case QQuick3DRuntimeLoader::QueryFilter::Cameras:
385 return QSSGRenderGraphObjectUtils::getBaseType(Type::OrthographicCamera);
386 case QQuick3DRuntimeLoader::QueryFilter::Lights:
387 return QSSGRenderGraphObjectUtils::getBaseType(Type::DirectionalLight);
388 case QQuick3DRuntimeLoader::QueryFilter::Models:
389 return QSSGRenderGraphObjectUtils::getBaseType(Type::Model);
390 }
391
392 Q_UNREACHABLE_RETURN(QSSGRenderGraphObject::BaseType(0));
393}
394
395/*!
396 \qmlmethod List<Object3D> RuntimeLoader::queryAll(QueryFilter filter)
397 \since 6.12
398
399 Returns a list of all objects matching the given filter.
400
401 The \a filter parameter specifies the type of objects to query for.
402 For example, to query for all materials, use the following code:
403
404 \badcode
405 var materials = runtimeLoader.queryAll(RuntimeLoader.Materials)
406 \endcode
407
408 The above code returns a list of all materials in the source asset file.
409*/
410
411QList<QQuick3DObject *> QQuick3DRuntimeLoader::queryAll(QueryFilter filter) const
412{
413 QList<QQuick3DObject *> results;
414 const auto range = m_objectsByType.equal_range(queryFilterToBaseType(filter));
415 for (auto it = range.first; it != range.second; ++it) {
416 if (auto *obj = it.value().data())
417 results << obj;
418 }
419 return results;
420}
421
422QT_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)