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