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
qmeshshape.cpp
Go to the documentation of this file.
1// Copyright (C) 2023 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6#include "qmeshshape_p.h"
8
9#include <QtQuick3D/QQuick3DGeometry>
10#include <extensions/PxExtensionsAPI.h>
11
12#include "foundation/PxVec3.h"
13#include "cooking/PxConvexMeshDesc.h"
14#include "extensions/PxDefaultStreams.h"
15
16#include <QtQml/qqml.h>
17#include <QtQml/qqmlcontext.h>
18
19#include <QtQuick3DUtils/private/qssgmesh_p.h>
20#include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
21#include <QtQuick3D/QQuick3DGeometry>
22
23#include "qmeshshape_p.h"
26
28
29static bool attributeBySemantic(const QQuick3DGeometry *geometry,
30 QQuick3DGeometry::Attribute::Semantic semantic,
31 QQuick3DGeometry::Attribute *result)
32{
33 for (int i = 0; i < geometry->attributeCount(); i++) {
34 const auto attr = geometry->attribute(i);
35 if (attr.semantic == semantic) {
36 *result = attr;
37 return true;
38 }
39 }
40
41 return false;
42};
43
44static bool isValidPositionLayout(int offset, int stride, qsizetype bufferSize)
45{
46 const qsizetype positionSize = qsizetype(3 * sizeof(float));
47 if (stride <= 0 || offset < 0 || offset + positionSize > stride || bufferSize < stride) {
48 qWarning() << "QQuick3DPhysicsMesh: Invalid geometry, position attribute (offset"
49 << offset << ") does not fit within stride" << stride << "and buffer size"
50 << bufferSize;
51 return false;
52 }
53 return true;
54}
55
57{
58 if (m_convexMesh != nullptr)
59 return m_convexMesh;
60
61 physx::PxPhysics *thePhysics = QPhysicsWorld::getPhysics();
62 if (thePhysics == nullptr)
63 return nullptr;
64
65 if (m_meshGeometry)
66 return convexMeshGeometrySource();
67 if (!m_meshPath.isEmpty())
68 return convexMeshQmlSource();
69 return nullptr;
70}
71
73{
74 if (m_triangleMesh != nullptr)
75 return m_triangleMesh;
76
77 physx::PxPhysics *thePhysics = QPhysicsWorld::getPhysics();
78 if (thePhysics == nullptr)
79 return nullptr;
80
81 if (m_meshGeometry)
82 return triangleMeshGeometrySource();
83 if (!m_meshPath.isEmpty())
84 return triangleMeshQmlSource();
85 return nullptr;
86}
87
88physx::PxConvexMesh *QQuick3DPhysicsMesh::convexMeshQmlSource()
89{
90 physx::PxPhysics *thePhysics = QPhysicsWorld::getPhysics();
91
92 m_convexMesh = QCacheUtils::readCachedConvexMesh(m_meshPath, *thePhysics);
93 if (m_convexMesh != nullptr)
94 return m_convexMesh;
95
96 m_convexMesh = QCacheUtils::readCookedConvexMesh(m_meshPath, *thePhysics);
97 if (m_convexMesh != nullptr)
98 return m_convexMesh;
99
100 loadSsgMesh();
101
102 if (!m_ssgMesh.isValid())
103 return nullptr;
104
105 const int vStride = m_ssgMesh.vertexBuffer().stride;
106 if (!isValidPositionLayout(m_posOffset, vStride, m_ssgMesh.vertexBuffer().data.size()))
107 return nullptr;
108 const int vCount = m_ssgMesh.vertexBuffer().data.size() / vStride;
109
110 qCDebug(lcQuick3dPhysics) << "prepare cooking" << vCount << "verts";
111
112 physx::PxConvexMeshDesc convexDesc;
113 convexDesc.points.count = vCount;
114 convexDesc.points.stride = vStride;
115 convexDesc.points.data = m_ssgMesh.vertexBuffer().data.constData() + m_posOffset;
116 convexDesc.flags = physx::PxConvexFlag::eCOMPUTE_CONVEX;
117
118 // NOTE: Since we are making a mesh for the convex hull and are only
119 // interested in the positions we can Skip the index array.
120
121 physx::PxDefaultMemoryOutputStream buf;
122 physx::PxConvexMeshCookingResult::Enum result;
123 const auto cooking = QPhysicsWorld::getCooking();
124 if (cooking && cooking->cookConvexMesh(convexDesc, buf, &result)) {
125 auto size = buf.getSize();
126 auto *data = buf.getData();
127 physx::PxDefaultMemoryInputData input(data, size);
128 m_convexMesh = thePhysics->createConvexMesh(input);
129 qCDebug(lcQuick3dPhysics) << "Created convex mesh" << m_convexMesh << "for mesh" << this;
130 QCacheUtils::writeCachedConvexMesh(m_meshPath, buf);
131 } else {
132 qCWarning(lcQuick3dPhysics) << "Could not create convex mesh from" << m_meshPath;
133 }
134
135 return m_convexMesh;
136}
137
138physx::PxConvexMesh *QQuick3DPhysicsMesh::convexMeshGeometrySource()
139{
140 auto vertexBuffer = m_meshGeometry->vertexData();
141
142 if (m_meshGeometry->primitiveType() != QQuick3DGeometry::PrimitiveType::Triangles) {
143 qWarning() << "QQuick3DPhysicsMesh: Invalid geometry primitive type, must be Triangles. ";
144 return nullptr;
145 }
146
147 if (!vertexBuffer.size()) {
148 qWarning() << "QQuick3DPhysicsMesh: Invalid geometry, vertexData is empty. ";
149 return nullptr;
150 }
151
152 QQuick3DGeometry::Attribute vertexAttribute;
153 if (!attributeBySemantic(m_meshGeometry, QQuick3DGeometry::Attribute::PositionSemantic,
154 &vertexAttribute)) {
155 qWarning() << "QQuick3DPhysicsMesh: Invalid geometry, no position attribute found.";
156 return nullptr;
157 }
158 Q_ASSERT(vertexAttribute.componentType == QQuick3DGeometry::Attribute::F32Type);
159
160 const auto stride = m_meshGeometry->stride();
161 if (!isValidPositionLayout(vertexAttribute.offset, stride, vertexBuffer.size()))
162 return nullptr;
163 const auto numVertices = vertexBuffer.size() / stride;
164
165 physx::PxConvexMeshDesc convexDesc;
166 convexDesc.points.count = numVertices;
167 convexDesc.points.stride = stride;
168 convexDesc.points.data = vertexBuffer.constData() + vertexAttribute.offset;
169 convexDesc.flags = physx::PxConvexFlag::eCOMPUTE_CONVEX;
170
171 // NOTE: Since we are making a mesh for the convex hull and are only
172 // interested in the positions we can Skip the index array.
173
174 const auto cooking = QPhysicsWorld::getCooking();
175 physx::PxDefaultMemoryOutputStream buf;
176 physx::PxConvexMeshCookingResult::Enum result;
177 if (cooking && cooking->cookConvexMesh(convexDesc, buf, &result)) {
178 auto size = buf.getSize();
179 auto *data = buf.getData();
180 physx::PxDefaultMemoryInputData input(data, size);
181 m_convexMesh = QPhysicsWorld::getPhysics()->createConvexMesh(input);
182 qCDebug(lcQuick3dPhysics) << "Created convex mesh" << m_convexMesh << "for mesh" << this;
183 } else {
184 qCWarning(lcQuick3dPhysics) << "Could not create convex mesh for" << this;
185 }
186
187 return m_convexMesh;
188}
189
190physx::PxTriangleMesh *QQuick3DPhysicsMesh::triangleMeshQmlSource()
191{
192 physx::PxPhysics *thePhysics = QPhysicsWorld::getPhysics();
193
194 m_triangleMesh = QCacheUtils::readCachedTriangleMesh(m_meshPath, *thePhysics);
195 if (m_triangleMesh != nullptr)
196 return m_triangleMesh;
197
198 m_triangleMesh = QCacheUtils::readCookedTriangleMesh(m_meshPath, *thePhysics);
199 if (m_triangleMesh != nullptr)
200 return m_triangleMesh;
201
202 loadSsgMesh();
203 if (!m_ssgMesh.isValid())
204 return nullptr;
205
206 auto vertexBuffer = m_ssgMesh.vertexBuffer().data;
207
208 const int posOffset = m_posOffset;
209 const auto stride = m_ssgMesh.vertexBuffer().stride;
210 if (!isValidPositionLayout(posOffset, stride, vertexBuffer.size()))
211 return nullptr;
212 const auto numVertices = vertexBuffer.size() / stride;
213
214 physx::PxTriangleMeshDesc triangleDesc;
215 triangleDesc.points.count = numVertices;
216 triangleDesc.points.stride = stride;
217 triangleDesc.points.data = vertexBuffer.constData() + posOffset;
218
219 auto indexBuffer = m_ssgMesh.indexBuffer().data;
220 if (indexBuffer.size()) {
221 const bool u16IndexType =
222 m_ssgMesh.indexBuffer().componentType == QSSGMesh::Mesh::ComponentType::UnsignedInt16;
223
224 Q_ASSERT(m_ssgMesh.indexBuffer().componentType
225 == QSSGMesh::Mesh::ComponentType::UnsignedInt16
226 || m_ssgMesh.indexBuffer().componentType
227 == QSSGMesh::Mesh::ComponentType::UnsignedInt32);
228
229 triangleDesc.triangles.data = indexBuffer.constData();
230 if (u16IndexType) {
231 triangleDesc.flags.set(physx::PxMeshFlag::e16_BIT_INDICES);
232 triangleDesc.triangles.stride = sizeof(quint16) * 3;
233 } else {
234 triangleDesc.triangles.stride = sizeof(quint32) * 3;
235 }
236 triangleDesc.triangles.count = indexBuffer.size() / triangleDesc.triangles.stride;
237 }
238
239 physx::PxDefaultMemoryOutputStream buf;
240 physx::PxTriangleMeshCookingResult::Enum result;
241 const auto cooking = QPhysicsWorld::getCooking();
242 if (cooking && cooking->cookTriangleMesh(triangleDesc, buf, &result)) {
243 auto size = buf.getSize();
244 auto *data = buf.getData();
245 physx::PxDefaultMemoryInputData input(data, size);
246 m_triangleMesh = thePhysics->createTriangleMesh(input);
247 qCDebug(lcQuick3dPhysics) << "Created triangle mesh" << m_triangleMesh << "for mesh"
248 << this;
250 } else {
251 qCWarning(lcQuick3dPhysics) << "Could not create triangle mesh from" << m_meshPath;
252 }
253
254 return m_triangleMesh;
255}
256
257physx::PxTriangleMesh *QQuick3DPhysicsMesh::triangleMeshGeometrySource()
258{
259 auto vertexBuffer = m_meshGeometry->vertexData();
260
261 if (m_meshGeometry->primitiveType() != QQuick3DGeometry::PrimitiveType::Triangles) {
262 qWarning() << "QQuick3DPhysicsMesh: Invalid geometry primitive type, must be Triangles. ";
263 return nullptr;
264 }
265
266 if (!vertexBuffer.size()) {
267 qWarning() << "QQuick3DPhysicsMesh: Invalid geometry, vertexData is empty. ";
268 return nullptr;
269 }
270
271 QQuick3DGeometry::Attribute vertexAttribute;
272 if (!attributeBySemantic(m_meshGeometry, QQuick3DGeometry::Attribute::PositionSemantic,
273 &vertexAttribute)) {
274 qWarning() << "QQuick3DPhysicsMesh: Invalid geometry, no position attribute found.";
275 return nullptr;
276 }
277 Q_ASSERT(vertexAttribute.componentType == QQuick3DGeometry::Attribute::F32Type);
278
279 const int posOffset = vertexAttribute.offset;
280 const auto stride = m_meshGeometry->stride();
281 if (!isValidPositionLayout(posOffset, stride, vertexBuffer.size()))
282 return nullptr;
283 const auto numVertices = vertexBuffer.size() / stride;
284
285 physx::PxTriangleMeshDesc triangleDesc;
286 triangleDesc.points.count = numVertices;
287 triangleDesc.points.stride = stride;
288 triangleDesc.points.data = vertexBuffer.constData() + posOffset;
289
290 auto indexBuffer = m_meshGeometry->indexData();
291 if (indexBuffer.size()) {
292 QQuick3DGeometry::Attribute indexAttribute;
293 if (!attributeBySemantic(m_meshGeometry, QQuick3DGeometry::Attribute::IndexSemantic,
294 &indexAttribute)) {
295 qWarning() << "QQuick3DPhysicsMesh: Invalid geometry, index data present but no "
296 "index attribute found.";
297 return nullptr;
298 }
299 const bool u16IndexType =
300 indexAttribute.componentType == QQuick3DGeometry::Attribute::U16Type;
301
302 Q_ASSERT(indexAttribute.componentType == QQuick3DGeometry::Attribute::U16Type
303 || indexAttribute.componentType == QQuick3DGeometry::Attribute::U32Type);
304
305 triangleDesc.triangles.data = indexBuffer.constData();
306 if (u16IndexType) {
307 triangleDesc.flags.set(physx::PxMeshFlag::e16_BIT_INDICES);
308 triangleDesc.triangles.stride = sizeof(quint16) * 3;
309 } else {
310 triangleDesc.triangles.stride = sizeof(quint32) * 3;
311 }
312 triangleDesc.triangles.count = indexBuffer.size() / triangleDesc.triangles.stride;
313 }
314
315 physx::PxDefaultMemoryOutputStream buf;
316 physx::PxTriangleMeshCookingResult::Enum result;
317 const auto cooking = QPhysicsWorld::getCooking();
318 if (cooking && cooking->cookTriangleMesh(triangleDesc, buf, &result)) {
319 auto size = buf.getSize();
320 auto *data = buf.getData();
321 physx::PxDefaultMemoryInputData input(data, size);
322 m_triangleMesh = QPhysicsWorld::getPhysics()->createTriangleMesh(input);
323 qCDebug(lcQuick3dPhysics) << "Created triangle mesh" << m_triangleMesh << "for mesh"
324 << this;
325 } else {
326 qCWarning(lcQuick3dPhysics) << "Could not create triangle mesh for" << this;
327 }
328
329 return m_triangleMesh;
330}
331
332void QQuick3DPhysicsMesh::loadSsgMesh()
333{
334 if (m_ssgMesh.isValid())
335 return;
336
337 // A bit ugly to use QSSGRenderPath here but it is just a wrapper for
338 // a QString and its hash.
339 // Security note: m_meshPath is a user provided file but QSSGBufferManager::loadMeshData is
340 // assumed to handle invalid meshes
341 m_ssgMesh = QSSGBufferManager::loadMeshData(QSSGRenderPath(m_meshPath));
342
343 static const char *compTypes[] = { "Null", "UnsignedInt8", "Int8", "UnsignedInt16",
344 "Int16", "UnsignedInt32", "Int32", "UnsignedInt64",
345 "Int64", "Float16", "Float32", "Float64" };
346
347 qCDebug(lcQuick3dPhysics) << "Loaded SSG mesh from" << m_meshPath << m_ssgMesh.isValid()
348 << "draw" << int(m_ssgMesh.drawMode()) << "wind"
349 << int(m_ssgMesh.winding()) << "subs" << m_ssgMesh.subsets().count()
350 << "attrs" << m_ssgMesh.vertexBuffer().entries.count()
351 << m_ssgMesh.vertexBuffer().data.size() << "stride"
352 << m_ssgMesh.vertexBuffer().stride << "verts"
353 << m_ssgMesh.vertexBuffer().data.size()
354 / m_ssgMesh.vertexBuffer().stride;
355
356 for (auto &v : m_ssgMesh.vertexBuffer().entries) {
357 qCDebug(lcQuick3dPhysics) << " attr" << v.name << compTypes[int(v.componentType)] << "cc"
358 << v.componentCount << "offs" << v.offset;
359 Q_ASSERT(v.componentType == QSSGMesh::Mesh::ComponentType::Float32);
360 if (v.name == "attr_pos")
361 m_posOffset = v.offset;
362 }
363
364 if (m_ssgMesh.isValid()) {
365 auto sub = m_ssgMesh.subsets().constFirst();
366 qCDebug(lcQuick3dPhysics) << "..." << sub.name << "count" << sub.count << "bounds"
367 << sub.bounds.min << sub.bounds.max << "offset" << sub.offset;
368 }
369
370#if 0 // EXTRA_DEBUG
371
372 int iStride = m_ssgMesh.indexBuffer().componentType == QSSGMesh::Mesh::ComponentType::UnsignedInt16 ? 2 : 4;
373 int vStride = m_ssgMesh.vertexBuffer().stride;
374 qDebug() << "IDX" << compTypes[int(m_ssgMesh.indexBuffer().componentType)] << m_ssgMesh.indexBuffer().data.size() / iStride;
375 const auto ib = m_ssgMesh.indexBuffer().data;
376 const auto vb = m_ssgMesh.vertexBuffer().data;
377
378 auto getPoint = [&vb, vStride, this](int idx) -> QVector3D {
379 auto *vp = vb.constData() + vStride * idx + m_posOffset;
380 return *reinterpret_cast<const QVector3D *>(vp);
381 return {};
382 };
383
384 if (iStride == 2) {
385
386 } else {
387 auto *ip = reinterpret_cast<const uint32_t *>(ib.data());
388 int n = ib.size() / iStride;
389 for (int i = 0; i < qMin(50,n); i += 3) {
390
391 qDebug() << " " << ip [i] << ip[i+1] << ip[i+2] << " --- "
392 << getPoint(ip[i]) << getPoint(ip[i+1]) << getPoint(ip[i+2]);
393 }
394 }
395#endif
396 if (!m_ssgMesh.isValid())
397 qCWarning(lcQuick3dPhysics) << "Could not read mesh from" << m_meshPath;
398}
399
400QQuick3DPhysicsMesh *QQuick3DPhysicsMeshManager::getMesh(const QUrl &source, QObject *contextObject)
401{
402 const QString qmlSource = QQuick3DModel::translateMeshSource(source, contextObject);
403 auto *mesh = sourceMeshHash.value(qmlSource);
404 if (!mesh) {
405 mesh = new QQuick3DPhysicsMesh(qmlSource);
406 sourceMeshHash[qmlSource] = mesh;
407 }
408 mesh->ref();
409 return mesh;
410}
411
413{
414 auto *mesh = geometryMeshHash.value(source);
415 if (!mesh) {
416 mesh = new QQuick3DPhysicsMesh(source);
417 geometryMeshHash.insert(source, mesh);
418 }
419 mesh->ref();
420 return mesh;
421}
422
424{
425 if (mesh == nullptr || mesh->deref() > 0)
426 return;
427
428 qCDebug(lcQuick3dPhysics()) << "deleting mesh" << mesh;
429 erase_if(sourceMeshHash, [mesh](std::pair<const QString &, QQuick3DPhysicsMesh *&> h) {
430 return h.second == mesh;
431 });
432 erase_if(geometryMeshHash, [mesh](std::pair<QQuick3DGeometry *, QQuick3DPhysicsMesh *&> h) {
433 return h.second == mesh;
434 });
435 delete mesh;
436}
437
438QHash<QString, QQuick3DPhysicsMesh *> QQuick3DPhysicsMeshManager::sourceMeshHash;
439QHash<QQuick3DGeometry *, QQuick3DPhysicsMesh *> QQuick3DPhysicsMeshManager::geometryMeshHash;
440
441/////////////////////////////////////////////////////////////////////////////
442
443QMeshShape::~QMeshShape()
444{
445 delete m_convexGeometry;
446 if (m_mesh)
447 QQuick3DPhysicsMeshManager::releaseMesh(m_mesh);
448}
449
450physx::PxGeometry *QMeshShape::getPhysXGeometry()
451{
452 if (m_dirtyPhysx || m_scaleDirty)
453 updatePhysXGeometry();
454 if (shapeType() == MeshType::CONVEX)
455 return m_convexGeometry;
456 if (shapeType() == MeshType::TRIANGLE)
457 return m_triangleGeometry;
458
459 Q_UNREACHABLE_RETURN(nullptr);
460}
461
462void QMeshShape::updatePhysXGeometry()
463{
464 delete m_convexGeometry;
465 delete m_triangleGeometry;
466 m_convexGeometry = nullptr;
467 m_triangleGeometry = nullptr;
468
469 if (!m_mesh)
470 return;
471
472 auto *convexMesh = shapeType() == MeshType::CONVEX ? m_mesh->convexMesh() : nullptr;
473 auto *triangleMesh = shapeType() == MeshType::TRIANGLE ? m_mesh->triangleMesh() : nullptr;
474 if (!convexMesh && !triangleMesh)
475 return;
476
477 auto meshScale = sceneScale();
478 physx::PxMeshScale scale(physx::PxVec3(meshScale.x(), meshScale.y(), meshScale.z()),
479 physx::PxQuat(physx::PxIdentity));
480
481 if (!QPhysicsUtils::isFinite(meshScale)
482 || !(convexMesh ? scale.isValidForConvexMesh() : scale.isValidForTriangleMesh())) {
483 qWarning() << "MeshShape: scaled mesh scale" << meshScale << "is invalid, ignoring.";
484 m_dirtyPhysx = false;
485 return;
486 }
487
488 if (convexMesh)
489 m_convexGeometry = new physx::PxConvexMeshGeometry(convexMesh, scale);
490 if (triangleMesh)
491 m_triangleGeometry = new physx::PxTriangleMeshGeometry(triangleMesh, scale);
492
493 m_dirtyPhysx = false;
494}
495
496const QUrl &QMeshShape::source() const
497{
498 return m_meshSource;
499}
500
501void QMeshShape::setSource(const QUrl &newSource)
502{
503 if (m_meshSource == newSource)
504 return;
505 m_meshSource = newSource;
506
507 // If we get a new source and our mesh was from the old source
508 // (meaning it was NOT from a geometry) we deref
509 if (m_geometry == nullptr) {
510 QQuick3DPhysicsMeshManager::releaseMesh(m_mesh);
511 m_mesh = nullptr;
512 }
513
514 // Load new mesh only if we don't have a geometry as source
515 if (m_geometry == nullptr && !newSource.isEmpty())
516 m_mesh = QQuick3DPhysicsMeshManager::getMesh(m_meshSource, this);
517
518 updatePhysXGeometry();
519 m_dirtyPhysx = true;
520
521 emit needsRebuild(this);
522 emit sourceChanged();
523}
524
525QQuick3DGeometry *QMeshShape::geometry() const
526{
527 return m_geometry;
528}
529
530void QMeshShape::setGeometry(QQuick3DGeometry *newGeometry)
531{
532 if (m_geometry == newGeometry)
533 return;
534 if (m_geometry)
535 m_geometry->disconnect(this);
536
537 m_geometry = newGeometry;
538
539 if (m_geometry != nullptr) {
540 connect(m_geometry, &QObject::destroyed, this, &QMeshShape::geometryDestroyed);
541 connect(m_geometry, &QQuick3DGeometry::geometryChanged, this,
542 &QMeshShape::geometryContentChanged);
543 }
544
545 // New geometry means we get a new mesh so deref the old one
546 QQuick3DPhysicsMeshManager::releaseMesh(m_mesh);
547 m_mesh = nullptr;
548 if (m_geometry != nullptr)
549 m_mesh = QQuick3DPhysicsMeshManager::getMesh(m_geometry);
550 else if (!m_meshSource.isEmpty())
551 m_mesh = QQuick3DPhysicsMeshManager::getMesh(m_meshSource, this);
552
553 updatePhysXGeometry();
554 m_dirtyPhysx = true;
555 emit needsRebuild(this);
556 emit geometryChanged();
557}
558
559void QMeshShape::geometryDestroyed(QObject *geometry)
560{
561 Q_ASSERT(m_geometry == geometry);
562 // Set geometry to null and the old one will be disconnected and dereferenced
563 setGeometry(nullptr);
564}
565
566void QMeshShape::geometryContentChanged()
567{
568 Q_ASSERT(m_geometry != nullptr);
569 QQuick3DPhysicsMeshManager::releaseMesh(m_mesh);
570 m_mesh = QQuick3DPhysicsMeshManager::getMesh(m_geometry);
571
572 updatePhysXGeometry();
573 m_dirtyPhysx = true;
574 emit needsRebuild(this);
575}
576
577QT_END_NAMESPACE
static void releaseMesh(QQuick3DPhysicsMesh *mesh)
static QQuick3DPhysicsMesh * getMesh(QQuick3DGeometry *source)
QQuick3DPhysicsMesh(const QQuick3DGeometry *geometrySource)
physx::PxTriangleMesh * triangleMesh()
physx::PxConvexMesh * convexMesh()
void writeCachedConvexMesh(const QString &filePath, physx::PxDefaultMemoryOutputStream &buf)
void writeCachedTriangleMesh(const QString &filePath, physx::PxDefaultMemoryOutputStream &buf)
Combined button and popup list for selecting options.
static bool isValidPositionLayout(int offset, int stride, qsizetype bufferSize)
static QT_BEGIN_NAMESPACE bool attributeBySemantic(const QQuick3DGeometry *geometry, QQuick3DGeometry::Attribute::Semantic semantic, QQuick3DGeometry::Attribute *result)