1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qquick3druntimeloader_p.h"
5
6#include <QtQuick3DAssetUtils/private/qssgscenedesc_p.h>
7#include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
8#include <QtQuick3DAssetUtils/private/qssgrtutilities_p.h>
9#include <QtQuick3DAssetImport/private/qssgassetimportmanager_p.h>
10#include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
11
12/*!
13 \qmltype RuntimeLoader
14 \inherits Node
15 \inqmlmodule QtQuick3D.AssetUtils
16 \since 6.2
17 \brief Imports a 3D asset at runtime.
18
19 The RuntimeLoader type provides a way to load a 3D asset directly from source at runtime,
20 without converting it to QtQuick3D's internal format first.
21
22 Qt 6.2 supports the loading of glTF version 2.0 files in both in text (.gltf) and binary (.glb) formats.
23*/
24
25/*!
26 \qmlproperty url RuntimeLoader::source
27
28 This property holds the location of the source file containing the 3D asset.
29 Changing this property will unload the current asset and attempt to load an asset from
30 the given URL.
31
32 The success or failure of the load operation is indicated by \l status.
33*/
34
35/*!
36 \qmlproperty enumeration RuntimeLoader::status
37
38 This property holds the status of the latest load operation.
39
40 \value RuntimeLoader.Empty
41 No URL was specified.
42 \value RuntimeLoader.Success
43 The load operation was successful.
44 \value RuntimeLoader.Error
45 The load operation failed. A human-readable error message is provided by \l errorString.
46
47 \readonly
48*/
49
50/*!
51 \qmlproperty string RuntimeLoader::errorString
52
53 This property holds a human-readable string indicating the status of the latest load operation.
54
55 \readonly
56*/
57
58/*!
59 \qmlproperty Bounds RuntimeLoader::bounds
60
61 This property describes the extents of the bounding volume around the imported model.
62
63 \note The value may not be available before the first render
64
65 \readonly
66*/
67
68/*!
69 \qmlproperty Instancing RuntimeLoader::instancing
70
71 If this property is set, the imported model will not be rendered normally. Instead, a number of
72 instances will be rendered, as defined by the instance table.
73
74 See the \l{Instanced Rendering} overview documentation for more information.
75*/
76
77QT_BEGIN_NAMESPACE
78
79QQuick3DRuntimeLoader::QQuick3DRuntimeLoader(QQuick3DNode *parent)
80 : QQuick3DNode(parent)
81{
82
83}
84
85QUrl QQuick3DRuntimeLoader::source() const
86{
87 return m_source;
88}
89
90void QQuick3DRuntimeLoader::setSource(const QUrl &newSource)
91{
92 if (m_source == newSource)
93 return;
94
95 const QQmlContext *context = qmlContext(this);
96 auto resolvedUrl = (context ? context->resolvedUrl(newSource) : newSource);
97
98 if (m_source == resolvedUrl)
99 return;
100
101 m_source = resolvedUrl;
102 emit sourceChanged();
103
104 if (isComponentComplete())
105 loadSource();
106}
107
108void QQuick3DRuntimeLoader::componentComplete()
109{
110 QQuick3DNode::componentComplete();
111 loadSource();
112}
113
114static void boxBoundsRecursive(const QQuick3DNode *baseNode, const QQuick3DNode *node, QQuick3DBounds3 &accBounds)
115{
116 if (!node)
117 return;
118
119 if (auto *model = qobject_cast<const QQuick3DModel *>(object: node)) {
120 auto b = model->bounds();
121 for (const QVector3D point : b.bounds.toQSSGBoxPoints()) {
122 auto p = model->mapPositionToNode(node: const_cast<QQuick3DNode *>(baseNode), localPosition: point);
123 if (Q_UNLIKELY(accBounds.bounds.isEmpty()))
124 accBounds.bounds = { p, p };
125 else
126 accBounds.bounds.include(v: p);
127 }
128 }
129 for (auto *child : node->childItems())
130 boxBoundsRecursive(baseNode, node: qobject_cast<const QQuick3DNode *>(object: child), accBounds);
131}
132
133template<typename Func>
134static void applyToModels(QQuick3DObject *obj, Func &&lambda)
135{
136 if (!obj)
137 return;
138 for (auto *child : obj->childItems()) {
139 if (auto *model = qobject_cast<QQuick3DModel *>(object: child))
140 lambda(model);
141 applyToModels(child, lambda);
142 }
143}
144
145void QQuick3DRuntimeLoader::loadSource()
146{
147 delete m_root;
148 m_root.clear();
149 QSSGBufferManager::unregisterMeshData(assetId: m_assetId);
150
151 m_status = Status::Empty;
152 m_errorString = QStringLiteral("No file selected");
153 if (!m_source.isValid()) {
154 emit statusChanged();
155 emit errorStringChanged();
156 return;
157 }
158
159 QSSGAssetImportManager importManager;
160 QSSGSceneDesc::Scene scene;
161 QString error(QStringLiteral("Unknown error"));
162 auto result = importManager.importFile(url: m_source, scene, error: &error);
163
164 switch (result) {
165 case QSSGAssetImportManager::ImportState::Success:
166 m_errorString = QStringLiteral("Success!");
167 m_status = Status::Success;
168 break;
169 case QSSGAssetImportManager::ImportState::IoError:
170 m_errorString = QStringLiteral("IO Error: ") + error;
171 m_status = Status::Error;
172 break;
173 case QSSGAssetImportManager::ImportState::Unsupported:
174 m_errorString = QStringLiteral("Unsupported: ") + error;
175 m_status = Status::Error;
176 break;
177 }
178
179 emit statusChanged();
180 emit errorStringChanged();
181
182 if (m_status != Status::Success) {
183 m_source.clear();
184 emit sourceChanged();
185 return;
186 }
187
188 // We create a dummy root node here, as it will be the parent to the first-level nodes
189 // and resources. If we use 'this' those first-level nodes/resources won't be deleted
190 // when a new scene is loaded.
191 m_root = new QQuick3DNode(this);
192 m_imported = QSSGRuntimeUtils::createScene(parent&: *m_root, scene);
193 m_assetId = scene.id;
194 m_boundsDirty = true;
195 m_instancingChanged = m_instancing != nullptr;
196 updateModels();
197 // Cleanup scene before deleting.
198 scene.cleanup();
199}
200
201void QQuick3DRuntimeLoader::updateModels()
202{
203 if (m_instancingChanged) {
204 applyToModels(obj: m_imported, lambda: [this](QQuick3DModel *model) {
205 model->setInstancing(m_instancing);
206 model->setInstanceRoot(m_imported);
207 });
208 m_instancingChanged = false;
209 }
210}
211
212QQuick3DRuntimeLoader::Status QQuick3DRuntimeLoader::status() const
213{
214 return m_status;
215}
216
217QString QQuick3DRuntimeLoader::errorString() const
218{
219 return m_errorString;
220}
221
222QSSGRenderGraphObject *QQuick3DRuntimeLoader::updateSpatialNode(QSSGRenderGraphObject *node)
223{
224 auto *result = QQuick3DNode::updateSpatialNode(node);
225 if (m_boundsDirty)
226 QMetaObject::invokeMethod(object: this, function: &QQuick3DRuntimeLoader::boundsChanged, type: Qt::QueuedConnection);
227 return result;
228}
229
230void QQuick3DRuntimeLoader::calculateBounds()
231{
232 if (!m_imported || !m_boundsDirty)
233 return;
234
235 m_bounds.bounds.setEmpty();
236 boxBoundsRecursive(baseNode: m_imported, node: m_imported, accBounds&: m_bounds);
237 m_boundsDirty = false;
238}
239
240const QQuick3DBounds3 &QQuick3DRuntimeLoader::bounds() const
241{
242 if (m_boundsDirty) {
243 auto *that = const_cast<QQuick3DRuntimeLoader *>(this);
244 that->calculateBounds();
245 return that->m_bounds;
246 }
247
248 return m_bounds;
249}
250
251QQuick3DInstancing *QQuick3DRuntimeLoader::instancing() const
252{
253 return m_instancing;
254}
255
256void QQuick3DRuntimeLoader::setInstancing(QQuick3DInstancing *newInstancing)
257{
258 if (m_instancing == newInstancing)
259 return;
260
261 QQuick3DObjectPrivate::attachWatcher(context: this, setter: &QQuick3DRuntimeLoader::setInstancing,
262 newO: newInstancing, oldO: m_instancing);
263
264 m_instancing = newInstancing;
265 m_instancingChanged = true;
266 updateModels();
267 emit instancingChanged();
268}
269
270QT_END_NAMESPACE
271

source code of qtquick3d/src/assetutils/qquick3druntimeloader.cpp