1 | // Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB). |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qmesh.h" |
5 | #include "qmesh_p.h" |
6 | |
7 | #include <QtCore/private/qfactoryloader_p.h> |
8 | #include <QDebug> |
9 | #include <QFile> |
10 | #include <QFileInfo> |
11 | #include <QScopedPointer> |
12 | #include <QMimeDatabase> |
13 | #include <QMimeType> |
14 | #include <QtCore/QBuffer> |
15 | #include <Qt3DRender/QRenderAspect> |
16 | #include <Qt3DCore/QAspectEngine> |
17 | #include <Qt3DCore/private/qscene_p.h> |
18 | #include <Qt3DCore/private/qdownloadhelperservice_p.h> |
19 | #include <Qt3DCore/private/qurlhelper_p.h> |
20 | #include <Qt3DRender/private/qrenderaspect_p.h> |
21 | #include <Qt3DRender/private/nodemanagers_p.h> |
22 | #include <Qt3DRender/private/qgeometryloaderinterface_p.h> |
23 | #include <Qt3DRender/private/renderlogging_p.h> |
24 | #include <Qt3DRender/private/qgeometryloaderfactory_p.h> |
25 | #include <Qt3DRender/private/geometryrenderermanager_p.h> |
26 | |
27 | #include <algorithm> |
28 | |
29 | QT_BEGIN_NAMESPACE |
30 | |
31 | using namespace Qt3DCore; |
32 | |
33 | namespace Qt3DRender { |
34 | |
35 | Q_GLOBAL_STATIC_WITH_ARGS(QFactoryLoader, geometryLoader, (QGeometryLoaderFactory_iid, QLatin1String("/geometryloaders" ), Qt::CaseInsensitive)) |
36 | |
37 | QMeshPrivate::QMeshPrivate() |
38 | : QGeometryRendererPrivate() |
39 | , m_status(QMesh::None) |
40 | { |
41 | } |
42 | |
43 | QMeshPrivate *QMeshPrivate::get(QMesh *q) |
44 | { |
45 | return q->d_func(); |
46 | } |
47 | |
48 | void QMeshPrivate::setScene(Qt3DCore::QScene *scene) |
49 | { |
50 | QGeometryRendererPrivate::setScene(scene); |
51 | updateFunctor(); |
52 | } |
53 | |
54 | void QMeshPrivate::updateFunctor() |
55 | { |
56 | Q_Q(QMesh); |
57 | m_geometryFactory = QGeometryFactoryPtr(new MeshLoaderFunctor(q)); |
58 | update(); |
59 | } |
60 | |
61 | void QMeshPrivate::setStatus(QMesh::Status status) |
62 | { |
63 | if (m_status != status) { |
64 | Q_Q(QMesh); |
65 | m_status = status; |
66 | const bool wasBlocked = q->blockNotifications(block: true); |
67 | emit q->statusChanged(status); |
68 | q->blockNotifications(block: wasBlocked); |
69 | } |
70 | } |
71 | |
72 | /*! |
73 | * \qmltype Mesh |
74 | * \instantiates Qt3DRender::QMesh |
75 | * \inqmlmodule Qt3D.Render |
76 | * \brief A custom mesh loader. |
77 | * |
78 | * Loads mesh data from external files in a variety of formats. |
79 | * |
80 | * In Qt3D 5.9, Mesh supports the following formats: |
81 | * |
82 | * \list |
83 | * \li Wavefront OBJ |
84 | * \li Stanford Triangle Format PLY |
85 | * \li STL (STereoLithography) |
86 | * \endlist |
87 | * |
88 | * QMesh will also support the following format if the SDK is installed and the fbx geometry loader plugin is built and found. |
89 | * \list |
90 | * \li Autodesk FBX |
91 | * \endlist |
92 | */ |
93 | |
94 | /*! |
95 | * \qmlproperty url Mesh::source |
96 | * |
97 | * Holds the source url to the file containing the custom mesh. |
98 | */ |
99 | |
100 | /*! |
101 | * \qmlproperty string Mesh::meshName |
102 | * |
103 | * Filter indicating which part of the mesh should be loaded. |
104 | * |
105 | * If meshName is empty (the default), then the entire mesh is loaded. |
106 | * |
107 | * If meshName is a plain string, then only the sub-mesh matching that name, if present, will be loaded. |
108 | * |
109 | * If meshName is a regular expression, than all sub-meshes matching the expression will be loaded. |
110 | * |
111 | * \note Only Wavefront OBJ files support sub-meshes. |
112 | * |
113 | * \sa QRegularExpression |
114 | */ |
115 | |
116 | /*! |
117 | \qmlproperty enumeration Mesh::status |
118 | |
119 | Holds the status of the mesh loading. |
120 | \sa Qt3DRender::QMesh::Status |
121 | \readonly |
122 | */ |
123 | |
124 | /*! |
125 | * \class Qt3DRender::QMesh |
126 | * \inheaderfile Qt3DRender/QMesh |
127 | * \inmodule Qt3DRender |
128 | * |
129 | * \inherits Qt3DRender::QGeometryRenderer |
130 | * |
131 | * \brief A custom mesh loader. |
132 | * |
133 | * Loads mesh data from external files in a variety of formats. |
134 | * Qt3DRender::QMesh loads data into a single mesh. |
135 | * |
136 | * In Qt3D 5.9, QMesh supports the following formats: |
137 | * |
138 | * \list |
139 | * \li Wavefront OBJ |
140 | * \li Stanford Triangle Format PLY |
141 | * \li STL (STereoLithography) |
142 | * \endlist |
143 | * |
144 | * QMesh will also support the following format if the SDK is installed and the fbx geometry loader plugin is built and found: |
145 | * \list |
146 | * \li Autodesk FBX |
147 | * \endlist |
148 | * |
149 | * If you wish to load an entire scene made of several objects, you should rather use the Qt3DRender::QSceneLoader instead. |
150 | * |
151 | * \sa Qt3DRender::QSceneLoader |
152 | */ |
153 | |
154 | /*! |
155 | \enum Qt3DRender::QMesh::Status |
156 | |
157 | This enum identifies the status of shader used. |
158 | |
159 | \value None A source mesh hasn't been assigned a source yet |
160 | \value Loading The mesh geometry is loading |
161 | \value Ready The mesh geometry was successfully loaded |
162 | \value Error An error occurred while loading the mesh |
163 | */ |
164 | |
165 | /*! |
166 | * Constructs a new QMesh with \a parent. |
167 | */ |
168 | QMesh::QMesh(QNode *parent) |
169 | : QGeometryRenderer(*new QMeshPrivate, parent) |
170 | { |
171 | } |
172 | |
173 | /*! \internal */ |
174 | QMesh::~QMesh() |
175 | { |
176 | } |
177 | |
178 | /*! \internal */ |
179 | QMesh::QMesh(QMeshPrivate &dd, QNode *parent) |
180 | : QGeometryRenderer(dd, parent) |
181 | { |
182 | } |
183 | |
184 | void QMesh::setSource(const QUrl& source) |
185 | { |
186 | Q_D(QMesh); |
187 | if (d->m_source == source) |
188 | return; |
189 | d->m_source = source; |
190 | d->updateFunctor(); |
191 | const bool blocked = blockNotifications(block: true); |
192 | emit sourceChanged(source); |
193 | blockNotifications(block: blocked); |
194 | } |
195 | |
196 | /*! |
197 | * \property QMesh::source |
198 | * |
199 | * Holds the \a source url to the file containing the custom mesh. |
200 | */ |
201 | QUrl QMesh::source() const |
202 | { |
203 | Q_D(const QMesh); |
204 | return d->m_source; |
205 | } |
206 | |
207 | void QMesh::setMeshName(const QString &meshName) |
208 | { |
209 | Q_D(QMesh); |
210 | if (d->m_meshName == meshName) |
211 | return; |
212 | d->m_meshName = meshName; |
213 | d->updateFunctor(); |
214 | const bool blocked = blockNotifications(block: true); |
215 | emit meshNameChanged(meshName); |
216 | blockNotifications(block: blocked); |
217 | } |
218 | |
219 | /*! |
220 | * \property QMesh::meshName |
221 | * |
222 | * Holds the name of the mesh. |
223 | */ |
224 | QString QMesh::meshName() const |
225 | { |
226 | Q_D(const QMesh); |
227 | return d->m_meshName; |
228 | } |
229 | |
230 | /*! |
231 | \property QMesh::status |
232 | |
233 | Holds the status of the mesh loading. |
234 | \sa Qt3DRender::QMesh::Status |
235 | */ |
236 | QMesh::Status QMesh::status() const |
237 | { |
238 | Q_D(const QMesh); |
239 | return d->m_status; |
240 | } |
241 | |
242 | /*! |
243 | * \internal |
244 | */ |
245 | MeshLoaderFunctor::MeshLoaderFunctor(QMesh *mesh, const QByteArray &sourceData) |
246 | : QGeometryFactory() |
247 | , m_mesh(mesh->id()) |
248 | , m_sourcePath(mesh->source()) |
249 | , m_meshName(mesh->meshName()) |
250 | , m_sourceData(sourceData) |
251 | , m_nodeManagers(nullptr) |
252 | , m_downloaderService(nullptr) |
253 | , m_status(QMesh::None) |
254 | { |
255 | } |
256 | |
257 | /*! |
258 | * \internal |
259 | */ |
260 | Qt3DCore::QGeometry *MeshLoaderFunctor::operator()() |
261 | { |
262 | m_status = QMesh::Loading; |
263 | |
264 | if (m_sourcePath.isEmpty()) { |
265 | qCWarning(Render::Jobs) << Q_FUNC_INFO << "Mesh is empty, nothing to load" ; |
266 | m_status = QMesh::Error; |
267 | return nullptr; |
268 | } |
269 | |
270 | QStringList ext; |
271 | if (!Qt3DCore::QDownloadHelperService::isLocal(url: m_sourcePath)) { |
272 | if (m_sourceData.isEmpty()) { |
273 | if (m_mesh) { |
274 | // Output a warning in the case a user is calling the functor directly |
275 | // in the frontend |
276 | if (m_nodeManagers == nullptr || m_downloaderService == nullptr) { |
277 | qWarning() << "Mesh source points to a remote URL. Remotes meshes can only be loaded if the geometry is processed by the Qt3DRender backend" ; |
278 | m_status = QMesh::Error; |
279 | return nullptr; |
280 | } |
281 | Qt3DCore::QDownloadRequestPtr request(new MeshDownloadRequest(m_mesh, m_sourcePath, m_nodeManagers)); |
282 | m_downloaderService->submitRequest(request); |
283 | } |
284 | return nullptr; |
285 | } |
286 | |
287 | QMimeDatabase db; |
288 | QMimeType mtype = db.mimeTypeForData(data: m_sourceData); |
289 | if (mtype.isValid()) { |
290 | ext = mtype.suffixes(); |
291 | } |
292 | QFileInfo finfo(m_sourcePath.path()); |
293 | ext << finfo.suffix(); |
294 | ext.removeAll(t: QLatin1String("" )); |
295 | if (!ext.contains(str: QLatin1String("obj" ))) |
296 | ext << QLatin1String("obj" ); |
297 | } else { |
298 | QString filePath = Qt3DCore::QUrlHelper::urlToLocalFileOrQrc(url: m_sourcePath); |
299 | QFileInfo finfo(filePath); |
300 | if (finfo.suffix().isEmpty()) |
301 | ext << QLatin1String("obj" ); |
302 | else |
303 | ext << finfo.suffix(); |
304 | } |
305 | |
306 | QScopedPointer<QGeometryLoaderInterface> loader; |
307 | for (const QString &e: std::as_const(t&: ext)) { |
308 | loader.reset(other: qLoadPlugin<QGeometryLoaderInterface, QGeometryLoaderFactory>(loader: geometryLoader(), key: e)); |
309 | if (loader) |
310 | break; |
311 | } |
312 | if (!loader) { |
313 | qCWarning(Render::Jobs, "unsupported format encountered (%s)" , qPrintable(ext.join(QLatin1String(", " )))); |
314 | m_status = QMesh::Error; |
315 | return nullptr; |
316 | } |
317 | |
318 | if (m_sourceData.isEmpty()) { |
319 | QString filePath = Qt3DCore::QUrlHelper::urlToLocalFileOrQrc(url: m_sourcePath); |
320 | QFile file(filePath); |
321 | if (!file.open(flags: QIODevice::ReadOnly)) { |
322 | qCDebug(Render::Jobs) << "Could not open file" << filePath << "for reading" ; |
323 | m_status = QMesh::Error; |
324 | return nullptr; |
325 | } |
326 | |
327 | if (loader->load(ioDev: &file, subMesh: m_meshName)) { |
328 | Qt3DCore::QGeometry *geometry = loader->geometry(); |
329 | m_status = geometry != nullptr ? QMesh::Ready : QMesh::Error; |
330 | return geometry; |
331 | } |
332 | qCWarning(Render::Jobs) << Q_FUNC_INFO << "Mesh loading failure for:" << filePath; |
333 | } else { |
334 | QT_PREPEND_NAMESPACE(QBuffer) buffer(&m_sourceData); |
335 | if (!buffer.open(openMode: QIODevice::ReadOnly)) { |
336 | m_status = QMesh::Error; |
337 | return nullptr; |
338 | } |
339 | |
340 | if (loader->load(ioDev: &buffer, subMesh: m_meshName)) { |
341 | Qt3DCore::QGeometry *geometry = loader->geometry(); |
342 | m_status = geometry != nullptr ? QMesh::Ready : QMesh::Error; |
343 | return geometry; |
344 | } |
345 | |
346 | qCWarning(Render::Jobs) << Q_FUNC_INFO << "Mesh loading failure for:" << m_sourcePath; |
347 | } |
348 | |
349 | return nullptr; |
350 | } |
351 | |
352 | /*! |
353 | * \internal |
354 | */ |
355 | bool MeshLoaderFunctor::equals(const QGeometryFactory &other) const |
356 | { |
357 | const MeshLoaderFunctor *otherFunctor = functor_cast<MeshLoaderFunctor>(other: &other); |
358 | if (otherFunctor != nullptr) |
359 | return (otherFunctor->m_sourcePath == m_sourcePath && |
360 | otherFunctor->m_sourceData.isEmpty() == m_sourceData.isEmpty() && |
361 | otherFunctor->m_meshName == m_meshName && |
362 | otherFunctor->m_downloaderService == m_downloaderService && |
363 | otherFunctor->m_nodeManagers == m_nodeManagers); |
364 | return false; |
365 | } |
366 | |
367 | /*! |
368 | * \internal |
369 | */ |
370 | MeshDownloadRequest::MeshDownloadRequest(Qt3DCore::QNodeId mesh, QUrl source, Render::NodeManagers *managers) |
371 | : Qt3DCore::QDownloadRequest(source) |
372 | , m_mesh(mesh) |
373 | , m_nodeManagers(managers) |
374 | { |
375 | } |
376 | |
377 | // Called in Aspect Thread context (not a Qt3D AspectJob) |
378 | // We are sure that when this is called, no AspectJob are running |
379 | void MeshDownloadRequest::onCompleted() |
380 | { |
381 | if (cancelled() || !succeeded()) |
382 | return; |
383 | |
384 | if (!m_nodeManagers) |
385 | return; |
386 | |
387 | Render::GeometryRenderer *renderer = m_nodeManagers->geometryRendererManager()->lookupResource(id: m_mesh); |
388 | if (!renderer) |
389 | return; |
390 | |
391 | QGeometryFactoryPtr geometryFactory = renderer->geometryFactory(); |
392 | if (!geometryFactory.isNull() && geometryFactory->id() == Qt3DCore::functorTypeId<MeshLoaderFunctor>()) { |
393 | QSharedPointer<MeshLoaderFunctor> functor = qSharedPointerCast<MeshLoaderFunctor>(src: geometryFactory); |
394 | |
395 | // We make sure we are setting the result for the right request |
396 | // (the functor for the mesh could have changed in the meantime) |
397 | if (m_url == functor->sourcePath()) { |
398 | functor->setSourceData(m_data); |
399 | |
400 | // mark the component as dirty so that the functor runs again in the correct job |
401 | m_nodeManagers->geometryRendererManager()->addDirtyGeometryRenderer(bufferId: m_mesh); |
402 | } |
403 | } |
404 | } |
405 | |
406 | } // namespace Qt3DRender |
407 | |
408 | QT_END_NAMESPACE |
409 | |
410 | #include "moc_qmesh.cpp" |
411 | |