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