1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qphysicsworld_p.h" |
5 | |
6 | #include "physxnode/qabstractphysxnode_p.h" |
7 | #include "physxnode/qphysxworld_p.h" |
8 | #include "qabstractphysicsnode_p.h" |
9 | #include "qdebugdrawhelper_p.h" |
10 | #include "qphysicsutils_p.h" |
11 | #include "qstaticphysxobjects_p.h" |
12 | #include "qboxshape_p.h" |
13 | #include "qsphereshape_p.h" |
14 | #include "qconvexmeshshape_p.h" |
15 | #include "qtrianglemeshshape_p.h" |
16 | #include "qcharactercontroller_p.h" |
17 | #include "qcapsuleshape_p.h" |
18 | #include "qplaneshape_p.h" |
19 | #include "qheightfieldshape_p.h" |
20 | |
21 | #include "PxPhysicsAPI.h" |
22 | #include "cooking/PxCooking.h" |
23 | |
24 | #include <QtQuick3D/private/qquick3dobject_p.h> |
25 | #include <QtQuick3D/private/qquick3dnode_p.h> |
26 | #include <QtQuick3D/private/qquick3dmodel_p.h> |
27 | #include <QtQuick3D/private/qquick3ddefaultmaterial_p.h> |
28 | #include <QtQuick3DUtils/private/qssgutils_p.h> |
29 | |
30 | #include <QtEnvironmentVariables> |
31 | |
32 | #define PHYSX_ENABLE_PVD 0 |
33 | |
34 | QT_BEGIN_NAMESPACE |
35 | |
36 | /*! |
37 | \qmltype PhysicsWorld |
38 | \inqmlmodule QtQuick3D.Physics |
39 | \since 6.4 |
40 | \brief Controls the physics simulation. |
41 | |
42 | The PhysicsWorld type controls the physics simulation. This node is used to create an instance of the physics world as well |
43 | as define its properties. There can only be one physics world. All collision nodes in the qml |
44 | will get added automatically to the physics world. |
45 | */ |
46 | |
47 | /*! |
48 | \qmlproperty vector3d PhysicsWorld::gravity |
49 | This property defines the gravity vector of the physics world. |
50 | The default value is \c (0, -981, 0). Set the value to \c{Qt.vector3d(0, -9.81, 0)} if your |
51 | unit of measurement is meters and you are simulating Earth gravity. |
52 | */ |
53 | |
54 | /*! |
55 | \qmlproperty bool PhysicsWorld::running |
56 | This property starts or stops the physical simulation. The default value is \c true. |
57 | */ |
58 | |
59 | /*! |
60 | \qmlproperty bool PhysicsWorld::forceDebugDraw |
61 | This property enables debug drawing of all active shapes in the physics world. The default value |
62 | is \c false. |
63 | */ |
64 | |
65 | /*! |
66 | \qmlproperty bool PhysicsWorld::enableCCD |
67 | This property enables continuous collision detection. This will reduce the risk of bodies going |
68 | through other bodies at high velocities (also known as tunnelling). The default value is \c |
69 | false. |
70 | |
71 | \warning Using trigger bodies with CCD enabled is not supported and can result in missing or |
72 | false trigger reports. |
73 | */ |
74 | |
75 | /*! |
76 | \qmlproperty float PhysicsWorld::typicalLength |
77 | This property defines the approximate size of objects in the simulation. This is used to |
78 | estimate certain length-related tolerances. Objects much smaller or much larger than this |
79 | size may not behave properly. The default value is \c 100. |
80 | |
81 | Range: \c{[0, inf]} |
82 | */ |
83 | |
84 | /*! |
85 | \qmlproperty float PhysicsWorld::typicalSpeed |
86 | This property defines the typical magnitude of velocities of objects in simulation. This is used |
87 | to estimate whether a contact should be treated as bouncing or resting based on its impact |
88 | velocity, and a kinetic energy threshold below which the simulation may put objects to sleep. |
89 | |
90 | For normal physical environments, a good choice is the approximate speed of an object falling |
91 | under gravity for one second. The default value is \c 1000. |
92 | |
93 | Range: \c{[0, inf]} |
94 | */ |
95 | |
96 | /*! |
97 | \qmlproperty float PhysicsWorld::defaultDensity |
98 | This property defines the default density of dynamic objects, measured in kilograms per cubic |
99 | unit. This is equal to the weight of a cube with side \c 1. |
100 | |
101 | The default value is \c 0.001, corresponding to 1 g/cm³: the density of water. If your unit of |
102 | measurement is meters, a good value would be \c 1000. Note that only positive values are |
103 | allowed. |
104 | |
105 | Range: \c{(0, inf]} |
106 | */ |
107 | |
108 | /*! |
109 | \qmlproperty Node PhysicsWorld::viewport |
110 | This property defines the viewport where debug components will be drawn if \l{forceDebugDraw} |
111 | is enabled. If unset the \l{scene} node will be used. |
112 | |
113 | \sa forceDebugDraw, scene |
114 | */ |
115 | |
116 | /*! |
117 | \qmlproperty float PhysicsWorld::minimumTimestep |
118 | This property defines the minimum simulation timestep in milliseconds. The default value is |
119 | \c 16.667 which corresponds to \c 60 frames per second. |
120 | |
121 | Range: \c{[0, maximumTimestep]} |
122 | */ |
123 | |
124 | /*! |
125 | \qmlproperty float PhysicsWorld::maximumTimestep |
126 | This property defines the maximum simulation timestep in milliseconds. The default value is |
127 | \c 33.333 which corresponds to \c 30 frames per second. |
128 | |
129 | Range: \c{[0, inf]} |
130 | */ |
131 | |
132 | /*! |
133 | \qmlproperty Node PhysicsWorld::scene |
134 | |
135 | This property defines the top-most Node that contains all the nodes of the physical |
136 | simulation. All physics objects that are an ancestor of this node will be seen as part of this |
137 | PhysicsWorld. |
138 | |
139 | \note Using the same scene node for several PhysicsWorld is unsupported. |
140 | */ |
141 | |
142 | /*! |
143 | \qmlsignal PhysicsWorld::frameDone(float timestep) |
144 | \since 6.5 |
145 | |
146 | This signal is emitted when the physical simulation is done simulating a frame. The \a timestep |
147 | parameter is how long in milliseconds the timestep was in the simulation. |
148 | */ |
149 | |
150 | /*! |
151 | \qmlproperty int PhysicsWorld::numThreads |
152 | \since 6.7 |
153 | |
154 | This property defines the number of threads used for the physical simulation. This is how the |
155 | range of values are interpreted: |
156 | |
157 | \table |
158 | \header |
159 | \li Value |
160 | \li Range |
161 | \li Description |
162 | \row |
163 | \li Negative |
164 | \li \c{[-inf, -1]} |
165 | \li Automatic thread count. The application will try to query the number of threads from the |
166 | system. |
167 | \row |
168 | \li Zero |
169 | \li \c{{0}} |
170 | \li No threading, simulation will run sequentially. |
171 | \row |
172 | \li Positive |
173 | \li \c{[1, inf]} |
174 | \li Specific thread count. |
175 | \endtable |
176 | |
177 | The default value is \c{-1}, meaning automatic thread count. |
178 | |
179 | \note Once the scene has started running it is not possible to change the number of threads. |
180 | */ |
181 | |
182 | /*! |
183 | \qmlproperty bool PhysicsWorld::reportKinematicKinematicCollisions |
184 | \since 6.7 |
185 | |
186 | This property controls if collisions between pairs of \e{kinematic} dynamic rigid bodies will |
187 | trigger a contact report. |
188 | |
189 | The default value is \c{false}. |
190 | |
191 | \note Once the scene has started running it is not possible to change this setting. |
192 | \sa PhysicsWorld::reportStaticKinematicCollisions |
193 | \sa DynamicRigidBody |
194 | \sa PhysicsNode::bodyContact |
195 | */ |
196 | |
197 | /*! |
198 | \qmlproperty bool PhysicsWorld::reportStaticKinematicCollisions |
199 | \since 6.7 |
200 | |
201 | This property controls if collisions between a static rigid body and a \e{kinematic} dynamic |
202 | rigid body will trigger a contact report. |
203 | |
204 | The default value is \c{false}. |
205 | |
206 | \note Once the scene has started running it is not possible to change this setting. |
207 | \sa PhysicsWorld::reportKinematicKinematicCollisions |
208 | \sa StaticRigidBody |
209 | \sa DynamicRigidBody |
210 | \sa PhysicsNode::bodyContact |
211 | */ |
212 | |
213 | Q_LOGGING_CATEGORY(lcQuick3dPhysics, "qt.quick3d.physics" ); |
214 | |
215 | ///////////////////////////////////////////////////////////////////////////// |
216 | |
217 | class SimulationWorker : public QObject |
218 | { |
219 | Q_OBJECT |
220 | public: |
221 | SimulationWorker(QPhysXWorld *physx) : m_physx(physx) { } |
222 | |
223 | public slots: |
224 | void simulateFrame(float minTimestep, float maxTimestep) |
225 | { |
226 | if (!m_physx->isRunning) { |
227 | m_timer.start(); |
228 | m_physx->isRunning = true; |
229 | } |
230 | |
231 | // Assuming: 0 <= minTimestep <= maxTimestep |
232 | |
233 | constexpr auto MILLIONTH = 0.000001; |
234 | |
235 | // If not enough time has elapsed we sleep until it has |
236 | auto deltaMS = m_timer.nsecsElapsed() * MILLIONTH; |
237 | while (deltaMS < minTimestep) { |
238 | auto sleepUSecs = (minTimestep - deltaMS) * 1000.f; |
239 | QThread::usleep(sleepUSecs); |
240 | deltaMS = m_timer.nsecsElapsed() * MILLIONTH; |
241 | } |
242 | m_timer.restart(); |
243 | |
244 | auto deltaSecs = qMin(a: float(deltaMS), b: maxTimestep) * 0.001f; |
245 | m_physx->scene->simulate(elapsedTime: deltaSecs); |
246 | m_physx->scene->fetchResults(block: true); |
247 | |
248 | emit frameDone(deltaTime: deltaSecs); |
249 | } |
250 | |
251 | void simulateFrameDesignStudio(float minTimestep, float maxTimestep) |
252 | { |
253 | Q_UNUSED(minTimestep); |
254 | Q_UNUSED(maxTimestep); |
255 | auto sleepUSecs = 16 * 1000.f; // 16 ms |
256 | QThread::usleep(sleepUSecs); |
257 | emit frameDoneDesignStudio(); |
258 | } |
259 | |
260 | signals: |
261 | void frameDone(float deltaTime); |
262 | void frameDoneDesignStudio(); |
263 | |
264 | private: |
265 | QPhysXWorld *m_physx = nullptr; |
266 | QElapsedTimer m_timer; |
267 | }; |
268 | |
269 | ///////////////////////////////////////////////////////////////////////////// |
270 | |
271 | void QPhysicsWorld::DebugModelHolder::releaseMeshPointer() |
272 | { |
273 | if (auto base = static_cast<physx::PxBase *>(ptr); base) |
274 | base->release(); |
275 | ptr = nullptr; |
276 | } |
277 | |
278 | const QVector3D &QPhysicsWorld::DebugModelHolder::halfExtents() const |
279 | { |
280 | return data; |
281 | } |
282 | void QPhysicsWorld::DebugModelHolder::setHalfExtents(const QVector3D &halfExtents) |
283 | { |
284 | data = halfExtents; |
285 | } |
286 | float QPhysicsWorld::DebugModelHolder::radius() const |
287 | { |
288 | return data.x(); |
289 | } |
290 | void QPhysicsWorld::DebugModelHolder::setRadius(float radius) |
291 | { |
292 | data.setX(radius); |
293 | } |
294 | float QPhysicsWorld::DebugModelHolder::heightScale() const |
295 | { |
296 | return data.x(); |
297 | } |
298 | void QPhysicsWorld::DebugModelHolder::setHeightScale(float heightScale) |
299 | { |
300 | data.setX(heightScale); |
301 | } |
302 | float QPhysicsWorld::DebugModelHolder::halfHeight() const |
303 | { |
304 | return data.y(); |
305 | } |
306 | void QPhysicsWorld::DebugModelHolder::setHalfHeight(float halfHeight) |
307 | { |
308 | data.setY(halfHeight); |
309 | } |
310 | float QPhysicsWorld::DebugModelHolder::rowScale() const |
311 | { |
312 | return data.y(); |
313 | } |
314 | void QPhysicsWorld::DebugModelHolder::setRowScale(float rowScale) |
315 | { |
316 | data.setY(rowScale); |
317 | } |
318 | float QPhysicsWorld::DebugModelHolder::columnScale() const |
319 | { |
320 | return data.z(); |
321 | } |
322 | void QPhysicsWorld::DebugModelHolder::setColumnScale(float columnScale) |
323 | { |
324 | data.setZ(columnScale); |
325 | } |
326 | physx::PxConvexMesh *QPhysicsWorld::DebugModelHolder::getConvexMesh() |
327 | { |
328 | return static_cast<physx::PxConvexMesh *>(ptr); |
329 | } |
330 | void QPhysicsWorld::DebugModelHolder::setConvexMesh(physx::PxConvexMesh *mesh) |
331 | { |
332 | ptr = static_cast<void *>(mesh); |
333 | } |
334 | physx::PxTriangleMesh *QPhysicsWorld::DebugModelHolder::getTriangleMesh() |
335 | { |
336 | return static_cast<physx::PxTriangleMesh *>(ptr); |
337 | } |
338 | void QPhysicsWorld::DebugModelHolder::setTriangleMesh(physx::PxTriangleMesh *mesh) |
339 | { |
340 | ptr = static_cast<void *>(mesh); |
341 | } |
342 | physx::PxHeightField *QPhysicsWorld::DebugModelHolder::getHeightField() |
343 | { |
344 | return static_cast<physx::PxHeightField *>(ptr); |
345 | } |
346 | void QPhysicsWorld::DebugModelHolder::setHeightField(physx::PxHeightField *hf) |
347 | { |
348 | ptr = static_cast<physx::PxHeightField *>(hf); |
349 | } |
350 | |
351 | ///////////////////////////////////////////////////////////////////////////// |
352 | |
353 | struct QWorldManager |
354 | { |
355 | QVector<QPhysicsWorld *> worlds; |
356 | QVector<QAbstractPhysicsNode *> orphanNodes; |
357 | }; |
358 | |
359 | static QWorldManager worldManager = QWorldManager {}; |
360 | |
361 | void QPhysicsWorld::registerNode(QAbstractPhysicsNode *physicsNode) |
362 | { |
363 | auto world = getWorld(node: physicsNode); |
364 | if (world) { |
365 | world->m_newPhysicsNodes.push_back(t: physicsNode); |
366 | } else { |
367 | worldManager.orphanNodes.push_back(t: physicsNode); |
368 | } |
369 | } |
370 | |
371 | void QPhysicsWorld::deregisterNode(QAbstractPhysicsNode *physicsNode) |
372 | { |
373 | for (auto world : worldManager.worlds) { |
374 | world->m_newPhysicsNodes.removeAll(t: physicsNode); |
375 | QMutexLocker locker(&world->m_removedPhysicsNodesMutex); |
376 | if (physicsNode->m_backendObject) { |
377 | Q_ASSERT(physicsNode->m_backendObject->frontendNode == physicsNode); |
378 | physicsNode->m_backendObject->frontendNode = nullptr; |
379 | physicsNode->m_backendObject->isRemoved = true; |
380 | physicsNode->m_backendObject = nullptr; |
381 | } |
382 | world->m_removedPhysicsNodes.insert(value: physicsNode); |
383 | } |
384 | worldManager.orphanNodes.removeAll(t: physicsNode); |
385 | } |
386 | |
387 | void QPhysicsWorld::registerContact(QAbstractPhysicsNode *sender, QAbstractPhysicsNode *receiver, |
388 | const QVector<QVector3D> &positions, |
389 | const QVector<QVector3D> &impulses, |
390 | const QVector<QVector3D> &normals) |
391 | { |
392 | // Since collision callbacks happen in the physx simulation thread we need |
393 | // to store these callbacks. Otherwise, if an object is deleted in the same |
394 | // frame a 'onBodyContact' signal is enqueued and a crash will happen. |
395 | // Therefore we save these contact callbacks and run them at the end of the |
396 | // physics frame when we know if the objects are deleted or not. |
397 | |
398 | BodyContact contact; |
399 | contact.sender = sender; |
400 | contact.receiver = receiver; |
401 | contact.positions = positions; |
402 | contact.impulses = impulses; |
403 | contact.normals = normals; |
404 | |
405 | m_registeredContacts.push_back(t: contact); |
406 | } |
407 | |
408 | QPhysicsWorld::QPhysicsWorld(QObject *parent) : QObject(parent) |
409 | { |
410 | m_inDesignStudio = !qEnvironmentVariableIsEmpty(varName: "QML_PUPPET_MODE" ); |
411 | m_physx = new QPhysXWorld; |
412 | m_physx->createWorld(); |
413 | |
414 | worldManager.worlds.push_back(t: this); |
415 | matchOrphanNodes(); |
416 | } |
417 | |
418 | QPhysicsWorld::~QPhysicsWorld() |
419 | { |
420 | m_workerThread.quit(); |
421 | m_workerThread.wait(); |
422 | for (auto body : m_physXBodies) { |
423 | body->cleanup(m_physx); |
424 | delete body; |
425 | } |
426 | m_physx->deleteWorld(); |
427 | delete m_physx; |
428 | worldManager.worlds.removeAll(t: this); |
429 | } |
430 | |
431 | void QPhysicsWorld::classBegin() {} |
432 | |
433 | void QPhysicsWorld::componentComplete() |
434 | { |
435 | if ((!m_running && !m_inDesignStudio) || m_physicsInitialized) |
436 | return; |
437 | initPhysics(); |
438 | emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep); |
439 | } |
440 | |
441 | QVector3D QPhysicsWorld::gravity() const |
442 | { |
443 | return m_gravity; |
444 | } |
445 | |
446 | bool QPhysicsWorld::running() const |
447 | { |
448 | return m_running; |
449 | } |
450 | |
451 | bool QPhysicsWorld::forceDebugDraw() const |
452 | { |
453 | return m_forceDebugDraw; |
454 | } |
455 | |
456 | bool QPhysicsWorld::enableCCD() const |
457 | { |
458 | return m_enableCCD; |
459 | } |
460 | |
461 | float QPhysicsWorld::typicalLength() const |
462 | { |
463 | return m_typicalLength; |
464 | } |
465 | |
466 | float QPhysicsWorld::typicalSpeed() const |
467 | { |
468 | return m_typicalSpeed; |
469 | } |
470 | |
471 | bool QPhysicsWorld::isNodeRemoved(QAbstractPhysicsNode *object) |
472 | { |
473 | return m_removedPhysicsNodes.contains(value: object); |
474 | } |
475 | |
476 | void QPhysicsWorld::setGravity(QVector3D gravity) |
477 | { |
478 | if (m_gravity == gravity) |
479 | return; |
480 | |
481 | m_gravity = gravity; |
482 | if (m_physx->scene) { |
483 | m_physx->scene->setGravity(QPhysicsUtils::toPhysXType(qvec: m_gravity)); |
484 | } |
485 | emit gravityChanged(gravity: m_gravity); |
486 | } |
487 | |
488 | void QPhysicsWorld::setRunning(bool running) |
489 | { |
490 | if (m_running == running) |
491 | return; |
492 | |
493 | m_running = running; |
494 | if (!m_inDesignStudio) { |
495 | if (m_running && !m_physicsInitialized) |
496 | initPhysics(); |
497 | if (m_running) |
498 | emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep); |
499 | } |
500 | emit runningChanged(running: m_running); |
501 | } |
502 | |
503 | void QPhysicsWorld::setForceDebugDraw(bool forceDebugDraw) |
504 | { |
505 | if (m_forceDebugDraw == forceDebugDraw) |
506 | return; |
507 | |
508 | m_forceDebugDraw = forceDebugDraw; |
509 | if (!m_forceDebugDraw) |
510 | disableDebugDraw(); |
511 | else |
512 | updateDebugDraw(); |
513 | emit forceDebugDrawChanged(forceDebugDraw: m_forceDebugDraw); |
514 | } |
515 | |
516 | QQuick3DNode *QPhysicsWorld::viewport() const |
517 | { |
518 | return m_viewport; |
519 | } |
520 | |
521 | void QPhysicsWorld::setHasIndividualDebugDraw() |
522 | { |
523 | m_hasIndividualDebugDraw = true; |
524 | } |
525 | |
526 | void QPhysicsWorld::setViewport(QQuick3DNode *viewport) |
527 | { |
528 | if (m_viewport == viewport) |
529 | return; |
530 | |
531 | m_viewport = viewport; |
532 | |
533 | // TODO: test this |
534 | for (auto material : m_debugMaterials) |
535 | delete material; |
536 | m_debugMaterials.clear(); |
537 | |
538 | for (auto &holder : m_collisionShapeDebugModels) { |
539 | holder.releaseMeshPointer(); |
540 | delete holder.model; |
541 | } |
542 | m_collisionShapeDebugModels.clear(); |
543 | |
544 | emit viewportChanged(viewport: m_viewport); |
545 | } |
546 | |
547 | void QPhysicsWorld::setMinimumTimestep(float minTimestep) |
548 | { |
549 | if (qFuzzyCompare(p1: m_minTimestep, p2: minTimestep)) |
550 | return; |
551 | |
552 | if (minTimestep > m_maxTimestep) { |
553 | qWarning(msg: "Minimum timestep greater than maximum timestep, value clamped" ); |
554 | minTimestep = qMin(a: minTimestep, b: m_maxTimestep); |
555 | } |
556 | |
557 | if (minTimestep < 0.f) { |
558 | qWarning(msg: "Minimum timestep less than zero, value clamped" ); |
559 | minTimestep = qMax(a: minTimestep, b: 0.f); |
560 | } |
561 | |
562 | if (qFuzzyCompare(p1: m_minTimestep, p2: minTimestep)) |
563 | return; |
564 | |
565 | m_minTimestep = minTimestep; |
566 | emit minimumTimestepChanged(minimumTimestep: m_minTimestep); |
567 | } |
568 | |
569 | void QPhysicsWorld::setMaximumTimestep(float maxTimestep) |
570 | { |
571 | if (qFuzzyCompare(p1: m_maxTimestep, p2: maxTimestep)) |
572 | return; |
573 | |
574 | if (maxTimestep < 0.f) { |
575 | qWarning(msg: "Maximum timestep less than zero, value clamped" ); |
576 | maxTimestep = qMax(a: maxTimestep, b: 0.f); |
577 | } |
578 | |
579 | if (qFuzzyCompare(p1: m_maxTimestep, p2: maxTimestep)) |
580 | return; |
581 | |
582 | m_maxTimestep = maxTimestep; |
583 | emit maximumTimestepChanged(maxTimestep); |
584 | } |
585 | |
586 | void QPhysicsWorld::setupDebugMaterials(QQuick3DNode *sceneNode) |
587 | { |
588 | if (!m_debugMaterials.isEmpty()) |
589 | return; |
590 | |
591 | const int lineWidth = m_inDesignStudio ? 1 : 3; |
592 | |
593 | // These colors match the indices of DebugDrawBodyType enum |
594 | for (auto color : { QColorConstants::Svg::chartreuse, QColorConstants::Svg::cyan, |
595 | QColorConstants::Svg::lightsalmon, QColorConstants::Svg::red, |
596 | QColorConstants::Svg::blueviolet, QColorConstants::Svg::black }) { |
597 | auto debugMaterial = new QQuick3DDefaultMaterial(); |
598 | debugMaterial->setLineWidth(lineWidth); |
599 | debugMaterial->setParentItem(sceneNode); |
600 | debugMaterial->setParent(sceneNode); |
601 | debugMaterial->setDiffuseColor(color); |
602 | debugMaterial->setLighting(QQuick3DDefaultMaterial::NoLighting); |
603 | debugMaterial->setCullMode(QQuick3DMaterial::NoCulling); |
604 | m_debugMaterials.push_back(t: debugMaterial); |
605 | } |
606 | } |
607 | |
608 | void QPhysicsWorld::updateDebugDraw() |
609 | { |
610 | if (!(m_forceDebugDraw || m_hasIndividualDebugDraw)) { |
611 | // Nothing to draw, trash all previous models (if any) and return |
612 | for (auto &holder : m_collisionShapeDebugModels) { |
613 | holder.releaseMeshPointer(); |
614 | delete holder.model; |
615 | } |
616 | m_collisionShapeDebugModels.clear(); |
617 | return; |
618 | } |
619 | |
620 | // Use scene node if no viewport has been specified |
621 | auto sceneNode = m_viewport ? m_viewport : m_scene; |
622 | |
623 | if (sceneNode == nullptr) |
624 | return; |
625 | |
626 | setupDebugMaterials(sceneNode); |
627 | m_hasIndividualDebugDraw = false; |
628 | |
629 | // Store the collision shapes we have now so we can clear out the removed ones |
630 | QSet<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>> currentCollisionShapes; |
631 | currentCollisionShapes.reserve(size: m_collisionShapeDebugModels.size()); |
632 | |
633 | for (QAbstractPhysXNode *node : m_physXBodies) { |
634 | if (!node->debugGeometryCapability()) |
635 | continue; |
636 | |
637 | const auto &collisionShapes = node->frontendNode->getCollisionShapesList(); |
638 | const int materialIdx = static_cast<int>(node->getDebugDrawBodyType()); |
639 | const int length = collisionShapes.length(); |
640 | for (int idx = 0; idx < length; idx++) { |
641 | const auto collisionShape = collisionShapes[idx]; |
642 | |
643 | if (!m_forceDebugDraw && !collisionShape->enableDebugDraw()) |
644 | continue; |
645 | |
646 | DebugModelHolder &holder = |
647 | m_collisionShapeDebugModels[std::make_pair(x: collisionShape, y&: node)]; |
648 | auto &model = holder.model; |
649 | |
650 | currentCollisionShapes.insert(value: std::make_pair(x: collisionShape, y&: node)); |
651 | |
652 | m_hasIndividualDebugDraw = |
653 | m_hasIndividualDebugDraw || collisionShape->enableDebugDraw(); |
654 | |
655 | // Create/Update debug view infrastructure |
656 | if (!model) { |
657 | model = new QQuick3DModel(); |
658 | model->setParentItem(sceneNode); |
659 | model->setParent(sceneNode); |
660 | model->setCastsShadows(false); |
661 | model->setReceivesShadows(false); |
662 | model->setCastsReflections(false); |
663 | } |
664 | |
665 | model->setVisible(true); |
666 | |
667 | { // update or set material |
668 | auto material = m_debugMaterials[materialIdx]; |
669 | QQmlListReference materialsRef(model, "materials" ); |
670 | if (materialsRef.count() == 0 || materialsRef.at(0) != material) { |
671 | materialsRef.clear(); |
672 | materialsRef.append(material); |
673 | } |
674 | } |
675 | |
676 | // Special handling of CharacterController since it has collision shapes, |
677 | // but not PhysX shapes |
678 | if (qobject_cast<QCharacterController *>(object: node->frontendNode)) { |
679 | QCapsuleShape *capsuleShape = qobject_cast<QCapsuleShape *>(object: collisionShape); |
680 | if (!capsuleShape) |
681 | continue; |
682 | |
683 | const float radius = capsuleShape->diameter() * 0.5; |
684 | const float halfHeight = capsuleShape->height() * 0.5; |
685 | |
686 | if (!qFuzzyCompare(p1: radius, p2: holder.radius()) |
687 | || !qFuzzyCompare(p1: halfHeight, p2: holder.halfHeight())) { |
688 | auto geom = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight); |
689 | geom->setParent(model); |
690 | model->setGeometry(geom); |
691 | holder.setRadius(radius); |
692 | holder.setHalfHeight(halfHeight); |
693 | } |
694 | |
695 | model->setPosition(node->frontendNode->scenePosition()); |
696 | model->setRotation(node->frontendNode->sceneRotation() |
697 | * QQuaternion::fromEulerAngles(pitch: 0, yaw: 0, roll: 90)); |
698 | continue; |
699 | } |
700 | |
701 | if (node->shapes.length() < length) |
702 | continue; |
703 | |
704 | const auto physXShape = node->shapes[idx]; |
705 | auto localPose = physXShape->getLocalPose(); |
706 | |
707 | switch (physXShape->getGeometryType()) { |
708 | case physx::PxGeometryType::eBOX: { |
709 | physx::PxBoxGeometry boxGeometry; |
710 | physXShape->getBoxGeometry(geometry&: boxGeometry); |
711 | const auto &halfExtentsOld = holder.halfExtents(); |
712 | const auto halfExtents = QPhysicsUtils::toQtType(vec: boxGeometry.halfExtents); |
713 | if (!qFuzzyCompare(v1: halfExtentsOld, v2: halfExtents)) { |
714 | auto geom = QDebugDrawHelper::generateBoxGeometry(halfExtents); |
715 | geom->setParent(model); |
716 | model->setGeometry(geom); |
717 | holder.setHalfExtents(halfExtents); |
718 | } |
719 | |
720 | } |
721 | break; |
722 | |
723 | case physx::PxGeometryType::eSPHERE: { |
724 | physx::PxSphereGeometry sphereGeometry; |
725 | physXShape->getSphereGeometry(geometry&: sphereGeometry); |
726 | const float radius = holder.radius(); |
727 | if (!qFuzzyCompare(p1: sphereGeometry.radius, p2: radius)) { |
728 | auto geom = QDebugDrawHelper::generateSphereGeometry(radius: sphereGeometry.radius); |
729 | geom->setParent(model); |
730 | model->setGeometry(geom); |
731 | holder.setRadius(sphereGeometry.radius); |
732 | } |
733 | } |
734 | break; |
735 | |
736 | case physx::PxGeometryType::eCAPSULE: { |
737 | physx::PxCapsuleGeometry capsuleGeometry; |
738 | physXShape->getCapsuleGeometry(geometry&: capsuleGeometry); |
739 | const float radius = holder.radius(); |
740 | const float halfHeight = holder.halfHeight(); |
741 | |
742 | if (!qFuzzyCompare(p1: capsuleGeometry.radius, p2: radius) |
743 | || !qFuzzyCompare(p1: capsuleGeometry.halfHeight, p2: halfHeight)) { |
744 | auto geom = QDebugDrawHelper::generateCapsuleGeometry( |
745 | radius: capsuleGeometry.radius, halfHeight: capsuleGeometry.halfHeight); |
746 | geom->setParent(model); |
747 | model->setGeometry(geom); |
748 | holder.setRadius(capsuleGeometry.radius); |
749 | holder.setHalfHeight(capsuleGeometry.halfHeight); |
750 | } |
751 | } |
752 | break; |
753 | |
754 | case physx::PxGeometryType::ePLANE:{ |
755 | physx::PxPlaneGeometry planeGeometry; |
756 | physXShape->getPlaneGeometry(geometry&: planeGeometry); |
757 | // Special rotation |
758 | const QQuaternion rotation = |
759 | QPhysicsUtils::kMinus90YawRotation * QPhysicsUtils::toQtType(quat: localPose.q); |
760 | localPose = physx::PxTransform(localPose.p, QPhysicsUtils::toPhysXType(qquat: rotation)); |
761 | |
762 | if (model->geometry() == nullptr) { |
763 | auto geom = QDebugDrawHelper::generatePlaneGeometry(); |
764 | geom->setParent(model); |
765 | model->setGeometry(geom); |
766 | } |
767 | } |
768 | break; |
769 | |
770 | // For heightfield, convex mesh and triangle mesh we increase its reference count |
771 | // to make sure it does not get dereferenced and deleted so that the new mesh will |
772 | // have another memory address so we know when it has changed. |
773 | case physx::PxGeometryType::eHEIGHTFIELD: { |
774 | physx::PxHeightFieldGeometry heightFieldGeometry; |
775 | bool success = physXShape->getHeightFieldGeometry(geometry&: heightFieldGeometry); |
776 | Q_ASSERT(success); |
777 | const float heightScale = holder.heightScale(); |
778 | const float rowScale = holder.rowScale(); |
779 | const float columnScale = holder.columnScale(); |
780 | |
781 | if (auto heightField = holder.getHeightField(); |
782 | heightField && heightField != heightFieldGeometry.heightField) { |
783 | heightField->release(); |
784 | holder.setHeightField(nullptr); |
785 | } |
786 | |
787 | if (!qFuzzyCompare(p1: heightFieldGeometry.heightScale, p2: heightScale) |
788 | || !qFuzzyCompare(p1: heightFieldGeometry.rowScale, p2: rowScale) |
789 | || !qFuzzyCompare(p1: heightFieldGeometry.columnScale, p2: columnScale) |
790 | || !holder.getHeightField()) { |
791 | if (!holder.getHeightField()) { |
792 | heightFieldGeometry.heightField->acquireReference(); |
793 | holder.setHeightField(heightFieldGeometry.heightField); |
794 | } |
795 | auto geom = QDebugDrawHelper::generateHeightFieldGeometry( |
796 | heightField: heightFieldGeometry.heightField, heightScale: heightFieldGeometry.heightScale, |
797 | rowScale: heightFieldGeometry.rowScale, columnScale: heightFieldGeometry.columnScale); |
798 | geom->setParent(model); |
799 | model->setGeometry(geom); |
800 | holder.setHeightScale(heightFieldGeometry.heightScale); |
801 | holder.setRowScale(heightFieldGeometry.rowScale); |
802 | holder.setColumnScale(heightFieldGeometry.columnScale); |
803 | } |
804 | } |
805 | break; |
806 | |
807 | case physx::PxGeometryType::eCONVEXMESH: { |
808 | physx::PxConvexMeshGeometry convexMeshGeometry; |
809 | const bool success = physXShape->getConvexMeshGeometry(geometry&: convexMeshGeometry); |
810 | Q_ASSERT(success); |
811 | const auto rotation = convexMeshGeometry.scale.rotation * localPose.q; |
812 | localPose = physx::PxTransform(localPose.p, rotation); |
813 | model->setScale(QPhysicsUtils::toQtType(vec: convexMeshGeometry.scale.scale)); |
814 | |
815 | if (auto convexMesh = holder.getConvexMesh(); |
816 | convexMesh && convexMesh != convexMeshGeometry.convexMesh) { |
817 | convexMesh->release(); |
818 | holder.setConvexMesh(nullptr); |
819 | } |
820 | |
821 | if (!model->geometry() || !holder.getConvexMesh()) { |
822 | if (!holder.getConvexMesh()) { |
823 | convexMeshGeometry.convexMesh->acquireReference(); |
824 | holder.setConvexMesh(convexMeshGeometry.convexMesh); |
825 | } |
826 | auto geom = QDebugDrawHelper::generateConvexMeshGeometry( |
827 | convexMesh: convexMeshGeometry.convexMesh); |
828 | geom->setParent(model); |
829 | model->setGeometry(geom); |
830 | } |
831 | } |
832 | break; |
833 | |
834 | case physx::PxGeometryType::eTRIANGLEMESH: { |
835 | physx::PxTriangleMeshGeometry triangleMeshGeometry; |
836 | const bool success = physXShape->getTriangleMeshGeometry(geometry&: triangleMeshGeometry); |
837 | Q_ASSERT(success); |
838 | const auto rotation = triangleMeshGeometry.scale.rotation * localPose.q; |
839 | localPose = physx::PxTransform(localPose.p, rotation); |
840 | model->setScale(QPhysicsUtils::toQtType(vec: triangleMeshGeometry.scale.scale)); |
841 | |
842 | if (auto triangleMesh = holder.getTriangleMesh(); |
843 | triangleMesh && triangleMesh != triangleMeshGeometry.triangleMesh) { |
844 | triangleMesh->release(); |
845 | holder.setTriangleMesh(nullptr); |
846 | } |
847 | |
848 | if (!model->geometry() || !holder.getTriangleMesh()) { |
849 | if (!holder.getTriangleMesh()) { |
850 | triangleMeshGeometry.triangleMesh->acquireReference(); |
851 | holder.setTriangleMesh(triangleMeshGeometry.triangleMesh); |
852 | } |
853 | auto geom = QDebugDrawHelper::generateTriangleMeshGeometry( |
854 | triangleMesh: triangleMeshGeometry.triangleMesh); |
855 | geom->setParent(model); |
856 | model->setGeometry(geom); |
857 | } |
858 | } |
859 | break; |
860 | |
861 | case physx::PxGeometryType::eINVALID: |
862 | case physx::PxGeometryType::eGEOMETRY_COUNT: |
863 | // should not happen |
864 | Q_UNREACHABLE(); |
865 | } |
866 | |
867 | auto globalPose = node->getGlobalPose(); |
868 | auto finalPose = globalPose.transform(src: localPose); |
869 | |
870 | model->setRotation(QPhysicsUtils::toQtType(quat: finalPose.q)); |
871 | model->setPosition(QPhysicsUtils::toQtType(vec: finalPose.p)); |
872 | } |
873 | } |
874 | |
875 | // Remove old collision shapes |
876 | m_collisionShapeDebugModels.removeIf( |
877 | pred: [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>, |
878 | DebugModelHolder>::iterator it) { |
879 | if (!currentCollisionShapes.contains(value: it.key())) { |
880 | auto holder = it.value(); |
881 | holder.releaseMeshPointer(); |
882 | if (holder.model) |
883 | delete holder.model; |
884 | return true; |
885 | } |
886 | return false; |
887 | }); |
888 | } |
889 | |
890 | static void collectPhysicsNodes(QQuick3DObject *node, QList<QAbstractPhysicsNode *> &nodes) |
891 | { |
892 | if (auto shape = qobject_cast<QAbstractPhysicsNode *>(object: node)) { |
893 | nodes.push_back(t: shape); |
894 | return; |
895 | } |
896 | |
897 | for (QQuick3DObject *child : node->childItems()) |
898 | collectPhysicsNodes(node: child, nodes); |
899 | } |
900 | |
901 | void QPhysicsWorld::updateDebugDrawDesignStudio() |
902 | { |
903 | // Use scene node if no viewport has been specified |
904 | auto sceneNode = m_viewport ? m_viewport : m_scene; |
905 | |
906 | if (sceneNode == nullptr) |
907 | return; |
908 | |
909 | setupDebugMaterials(sceneNode); |
910 | |
911 | // Store the collision shapes we have now so we can clear out the removed ones |
912 | QSet<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>> currentCollisionShapes; |
913 | currentCollisionShapes.reserve(size: m_collisionShapeDebugModels.size()); |
914 | |
915 | QList<QAbstractPhysicsNode *> activePhysicsNodes; |
916 | activePhysicsNodes.reserve(size: m_collisionShapeDebugModels.size()); |
917 | collectPhysicsNodes(node: m_scene, nodes&: activePhysicsNodes); |
918 | |
919 | for (QAbstractPhysicsNode *node : activePhysicsNodes) { |
920 | |
921 | const auto &collisionShapes = node->getCollisionShapesList(); |
922 | const int materialIdx = 0; // Just take first material |
923 | const int length = collisionShapes.length(); |
924 | |
925 | const bool isCharacterController = qobject_cast<QCharacterController *>(object: node) != nullptr; |
926 | |
927 | for (int idx = 0; idx < length; idx++) { |
928 | QAbstractCollisionShape *collisionShape = collisionShapes[idx]; |
929 | DebugModelHolder &holder = |
930 | m_DesignStudioDebugModels[std::make_pair(x&: collisionShape, y&: node)]; |
931 | auto &model = holder.model; |
932 | |
933 | currentCollisionShapes.insert(value: std::make_pair(x&: collisionShape, y&: node)); |
934 | |
935 | m_hasIndividualDebugDraw = |
936 | m_hasIndividualDebugDraw || collisionShape->enableDebugDraw(); |
937 | |
938 | // Create/Update debug view infrastructure |
939 | { |
940 | // Hack: we have to delete the model every frame so it shows up in QDS |
941 | // whenever the code is updated, not sure why ¯\_(?)_/¯ |
942 | delete model; |
943 | model = new QQuick3DModel(); |
944 | model->setParentItem(sceneNode); |
945 | model->setParent(sceneNode); |
946 | model->setCastsShadows(false); |
947 | model->setReceivesShadows(false); |
948 | model->setCastsReflections(false); |
949 | } |
950 | |
951 | const bool hasGeometry = holder.geometry != nullptr; |
952 | QVector3D scenePosition = collisionShape->scenePosition(); |
953 | QQuaternion sceneRotation = collisionShape->sceneRotation(); |
954 | QQuick3DGeometry *newGeometry = nullptr; |
955 | |
956 | if (isCharacterController) |
957 | sceneRotation = sceneRotation * QQuaternion::fromEulerAngles(eulerAngles: QVector3D(0, 0, 90)); |
958 | |
959 | { // update or set material |
960 | auto material = m_debugMaterials[materialIdx]; |
961 | QQmlListReference materialsRef(model, "materials" ); |
962 | if (materialsRef.count() == 0 || materialsRef.at(0) != material) { |
963 | materialsRef.clear(); |
964 | materialsRef.append(material); |
965 | } |
966 | } |
967 | |
968 | if (auto shape = qobject_cast<QBoxShape *>(object: collisionShape)) { |
969 | const auto &halfExtentsOld = holder.halfExtents(); |
970 | const auto halfExtents = shape->sceneScale() * shape->extents() * 0.5f; |
971 | if (!qFuzzyCompare(v1: halfExtentsOld, v2: halfExtents) || !hasGeometry) { |
972 | newGeometry = QDebugDrawHelper::generateBoxGeometry(halfExtents); |
973 | holder.setHalfExtents(halfExtents); |
974 | } |
975 | } else if (auto shape = qobject_cast<QSphereShape *>(object: collisionShape)) { |
976 | const float radiusOld = holder.radius(); |
977 | const float radius = shape->sceneScale().x() * shape->diameter() * 0.5f; |
978 | if (!qFuzzyCompare(p1: radiusOld, p2: radius) || !hasGeometry) { |
979 | newGeometry = QDebugDrawHelper::generateSphereGeometry(radius); |
980 | holder.setRadius(radius); |
981 | } |
982 | } else if (auto shape = qobject_cast<QCapsuleShape *>(object: collisionShape)) { |
983 | const float radiusOld = holder.radius(); |
984 | const float halfHeightOld = holder.halfHeight(); |
985 | const float radius = shape->sceneScale().y() * shape->diameter() * 0.5f; |
986 | const float halfHeight = shape->sceneScale().x() * shape->height() * 0.5f; |
987 | |
988 | if ((!qFuzzyCompare(p1: radiusOld, p2: radius) || !qFuzzyCompare(p1: halfHeightOld, p2: halfHeight)) |
989 | || !hasGeometry) { |
990 | newGeometry = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight); |
991 | holder.setRadius(radius); |
992 | holder.setHalfHeight(halfHeight); |
993 | } |
994 | } else if (qobject_cast<QPlaneShape *>(object: collisionShape)) { |
995 | if (!hasGeometry) |
996 | newGeometry = QDebugDrawHelper::generatePlaneGeometry(); |
997 | } else if (auto shape = qobject_cast<QHeightFieldShape *>(object: collisionShape)) { |
998 | physx::PxHeightFieldGeometry *heightFieldGeometry = |
999 | static_cast<physx::PxHeightFieldGeometry *>(shape->getPhysXGeometry()); |
1000 | const float heightScale = holder.heightScale(); |
1001 | const float rowScale = holder.rowScale(); |
1002 | const float columnScale = holder.columnScale(); |
1003 | scenePosition += shape->hfOffset(); |
1004 | if (!heightFieldGeometry) { |
1005 | qWarning() << "Could not get height field" ; |
1006 | } else if (!qFuzzyCompare(p1: heightFieldGeometry->heightScale, p2: heightScale) |
1007 | || !qFuzzyCompare(p1: heightFieldGeometry->rowScale, p2: rowScale) |
1008 | || !qFuzzyCompare(p1: heightFieldGeometry->columnScale, p2: columnScale) |
1009 | || !hasGeometry) { |
1010 | newGeometry = QDebugDrawHelper::generateHeightFieldGeometry( |
1011 | heightField: heightFieldGeometry->heightField, heightScale: heightFieldGeometry->heightScale, |
1012 | rowScale: heightFieldGeometry->rowScale, columnScale: heightFieldGeometry->columnScale); |
1013 | holder.setHeightScale(heightFieldGeometry->heightScale); |
1014 | holder.setRowScale(heightFieldGeometry->rowScale); |
1015 | holder.setColumnScale(heightFieldGeometry->columnScale); |
1016 | } |
1017 | } else if (auto shape = qobject_cast<QConvexMeshShape *>(object: collisionShape)) { |
1018 | auto convexMeshGeometry = |
1019 | static_cast<physx::PxConvexMeshGeometry *>(shape->getPhysXGeometry()); |
1020 | if (!convexMeshGeometry) { |
1021 | qWarning() << "Could not get convex mesh" ; |
1022 | } else { |
1023 | model->setScale(QPhysicsUtils::toQtType(vec: convexMeshGeometry->scale.scale)); |
1024 | |
1025 | if (!hasGeometry) { |
1026 | newGeometry = QDebugDrawHelper::generateConvexMeshGeometry( |
1027 | convexMesh: convexMeshGeometry->convexMesh); |
1028 | } |
1029 | } |
1030 | } else if (auto shape = qobject_cast<QTriangleMeshShape *>(object: collisionShape)) { |
1031 | physx::PxTriangleMeshGeometry *triangleMeshGeometry = |
1032 | static_cast<physx::PxTriangleMeshGeometry *>(shape->getPhysXGeometry()); |
1033 | if (!triangleMeshGeometry) { |
1034 | qWarning() << "Could not get triangle mesh" ; |
1035 | } else { |
1036 | model->setScale(QPhysicsUtils::toQtType(vec: triangleMeshGeometry->scale.scale)); |
1037 | |
1038 | if (!hasGeometry) { |
1039 | newGeometry = QDebugDrawHelper::generateTriangleMeshGeometry( |
1040 | triangleMesh: triangleMeshGeometry->triangleMesh); |
1041 | } |
1042 | } |
1043 | } |
1044 | |
1045 | if (newGeometry) { |
1046 | delete holder.geometry; |
1047 | holder.geometry = newGeometry; |
1048 | } |
1049 | |
1050 | model->setGeometry(holder.geometry); |
1051 | model->setVisible(true); |
1052 | |
1053 | model->setRotation(sceneRotation); |
1054 | model->setPosition(scenePosition); |
1055 | } |
1056 | } |
1057 | |
1058 | // Remove old debug models |
1059 | m_DesignStudioDebugModels.removeIf( |
1060 | pred: [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>, |
1061 | DebugModelHolder>::iterator it) { |
1062 | if (!currentCollisionShapes.contains(value: it.key())) { |
1063 | auto holder = it.value(); |
1064 | holder.releaseMeshPointer(); |
1065 | if (holder.model) { |
1066 | delete holder.geometry; |
1067 | delete holder.model; |
1068 | } |
1069 | return true; |
1070 | } |
1071 | return false; |
1072 | }); |
1073 | } |
1074 | |
1075 | void QPhysicsWorld::disableDebugDraw() |
1076 | { |
1077 | m_hasIndividualDebugDraw = false; |
1078 | |
1079 | for (QAbstractPhysXNode *body : m_physXBodies) { |
1080 | const auto &collisionShapes = body->frontendNode->getCollisionShapesList(); |
1081 | const int length = collisionShapes.length(); |
1082 | for (int idx = 0; idx < length; idx++) { |
1083 | const auto collisionShape = collisionShapes[idx]; |
1084 | if (collisionShape->enableDebugDraw()) { |
1085 | m_hasIndividualDebugDraw = true; |
1086 | return; |
1087 | } |
1088 | } |
1089 | } |
1090 | } |
1091 | |
1092 | void QPhysicsWorld::setEnableCCD(bool enableCCD) |
1093 | { |
1094 | if (m_enableCCD == enableCCD) |
1095 | return; |
1096 | |
1097 | if (m_physicsInitialized) { |
1098 | qWarning() |
1099 | << "Warning: Changing 'enableCCD' after physics is initialized will have no effect" ; |
1100 | return; |
1101 | } |
1102 | |
1103 | m_enableCCD = enableCCD; |
1104 | emit enableCCDChanged(enableCCD: m_enableCCD); |
1105 | } |
1106 | |
1107 | void QPhysicsWorld::setTypicalLength(float typicalLength) |
1108 | { |
1109 | if (qFuzzyCompare(p1: typicalLength, p2: m_typicalLength)) |
1110 | return; |
1111 | |
1112 | if (typicalLength <= 0.f) { |
1113 | qWarning() << "Warning: 'typicalLength' value less than zero, ignored" ; |
1114 | return; |
1115 | } |
1116 | |
1117 | if (m_physicsInitialized) { |
1118 | qWarning() << "Warning: Changing 'typicalLength' after physics is initialized will have " |
1119 | "no effect" ; |
1120 | return; |
1121 | } |
1122 | |
1123 | m_typicalLength = typicalLength; |
1124 | |
1125 | emit typicalLengthChanged(typicalLength); |
1126 | } |
1127 | |
1128 | void QPhysicsWorld::setTypicalSpeed(float typicalSpeed) |
1129 | { |
1130 | if (qFuzzyCompare(p1: typicalSpeed, p2: m_typicalSpeed)) |
1131 | return; |
1132 | |
1133 | if (m_physicsInitialized) { |
1134 | qWarning() << "Warning: Changing 'typicalSpeed' after physics is initialized will have " |
1135 | "no effect" ; |
1136 | return; |
1137 | } |
1138 | |
1139 | m_typicalSpeed = typicalSpeed; |
1140 | |
1141 | emit typicalSpeedChanged(typicalSpeed); |
1142 | } |
1143 | |
1144 | float QPhysicsWorld::defaultDensity() const |
1145 | { |
1146 | return m_defaultDensity; |
1147 | } |
1148 | |
1149 | float QPhysicsWorld::minimumTimestep() const |
1150 | { |
1151 | return m_minTimestep; |
1152 | } |
1153 | |
1154 | float QPhysicsWorld::maximumTimestep() const |
1155 | { |
1156 | return m_maxTimestep; |
1157 | } |
1158 | |
1159 | void QPhysicsWorld::setDefaultDensity(float defaultDensity) |
1160 | { |
1161 | if (qFuzzyCompare(p1: m_defaultDensity, p2: defaultDensity)) |
1162 | return; |
1163 | m_defaultDensity = defaultDensity; |
1164 | |
1165 | // Go through all dynamic rigid bodies and update the default density |
1166 | for (QAbstractPhysXNode *body : m_physXBodies) |
1167 | body->updateDefaultDensity(density: m_defaultDensity); |
1168 | |
1169 | emit defaultDensityChanged(defaultDensity); |
1170 | } |
1171 | |
1172 | // Remove physics world items that no longer exist |
1173 | |
1174 | void QPhysicsWorld::cleanupRemovedNodes() |
1175 | { |
1176 | m_physXBodies.removeIf(pred: [this](QAbstractPhysXNode *body) { |
1177 | return body->cleanupIfRemoved(physX: m_physx); |
1178 | }); |
1179 | // We don't need to lock the mutex here since the simulation |
1180 | // worker is waiting |
1181 | m_removedPhysicsNodes.clear(); |
1182 | } |
1183 | |
1184 | void QPhysicsWorld::initPhysics() |
1185 | { |
1186 | Q_ASSERT(!m_physicsInitialized); |
1187 | |
1188 | const unsigned int numThreads = m_numThreads >= 0 ? m_numThreads : qMax(a: 0, b: QThread::idealThreadCount()); |
1189 | m_physx->createScene(typicalLength: m_typicalLength, typicalSpeed: m_typicalSpeed, gravity: m_gravity, enableCCD: m_enableCCD, physicsWorld: this, numThreads); |
1190 | |
1191 | // Setup worker thread |
1192 | SimulationWorker *worker = new SimulationWorker(m_physx); |
1193 | worker->moveToThread(thread: &m_workerThread); |
1194 | connect(sender: &m_workerThread, signal: &QThread::finished, context: worker, slot: &QObject::deleteLater); |
1195 | if (m_inDesignStudio) { |
1196 | connect(sender: this, signal: &QPhysicsWorld::simulateFrame, context: worker, |
1197 | slot: &SimulationWorker::simulateFrameDesignStudio); |
1198 | connect(sender: worker, signal: &SimulationWorker::frameDoneDesignStudio, context: this, |
1199 | slot: &QPhysicsWorld::frameFinishedDesignStudio); |
1200 | } else { |
1201 | connect(sender: this, signal: &QPhysicsWorld::simulateFrame, context: worker, slot: &SimulationWorker::simulateFrame); |
1202 | connect(sender: worker, signal: &SimulationWorker::frameDone, context: this, slot: &QPhysicsWorld::frameFinished); |
1203 | } |
1204 | m_workerThread.start(); |
1205 | |
1206 | m_physicsInitialized = true; |
1207 | } |
1208 | |
1209 | void QPhysicsWorld::frameFinished(float deltaTime) |
1210 | { |
1211 | matchOrphanNodes(); |
1212 | emitContactCallbacks(); |
1213 | cleanupRemovedNodes(); |
1214 | for (auto *node : std::as_const(t&: m_newPhysicsNodes)) { |
1215 | auto *body = node->createPhysXBackend(); |
1216 | body->init(world: this, physX: m_physx); |
1217 | m_physXBodies.push_back(t: body); |
1218 | } |
1219 | m_newPhysicsNodes.clear(); |
1220 | |
1221 | QHash<QQuick3DNode *, QMatrix4x4> transformCache; |
1222 | |
1223 | // TODO: Use dirty flag/dirty list to avoid redoing things that didn't change |
1224 | for (auto *physXBody : std::as_const(t&: m_physXBodies)) { |
1225 | physXBody->markDirtyShapes(); |
1226 | physXBody->rebuildDirtyShapes(this, m_physx); |
1227 | physXBody->updateFilters(); |
1228 | |
1229 | // Sync the physics world and the scene |
1230 | physXBody->sync(deltaTime, transformCache); |
1231 | } |
1232 | |
1233 | updateDebugDraw(); |
1234 | |
1235 | if (m_running) |
1236 | emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep); |
1237 | emit frameDone(timestep: deltaTime * 1000); |
1238 | } |
1239 | |
1240 | void QPhysicsWorld::frameFinishedDesignStudio() |
1241 | { |
1242 | // Note sure if this is needed but do it anyway |
1243 | matchOrphanNodes(); |
1244 | emitContactCallbacks(); |
1245 | cleanupRemovedNodes(); |
1246 | // Ignore new physics nodes, we find them from the scene node anyway |
1247 | m_newPhysicsNodes.clear(); |
1248 | |
1249 | updateDebugDrawDesignStudio(); |
1250 | |
1251 | emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep); |
1252 | } |
1253 | |
1254 | QPhysicsWorld *QPhysicsWorld::getWorld(QQuick3DNode *node) |
1255 | { |
1256 | for (QPhysicsWorld *world : worldManager.worlds) { |
1257 | if (!world->m_scene) { |
1258 | continue; |
1259 | } |
1260 | |
1261 | QQuick3DNode *nodeCurr = node; |
1262 | |
1263 | // Maybe pointless but check starting node |
1264 | if (nodeCurr == world->m_scene) |
1265 | return world; |
1266 | |
1267 | while (nodeCurr->parentNode()) { |
1268 | nodeCurr = nodeCurr->parentNode(); |
1269 | if (nodeCurr == world->m_scene) |
1270 | return world; |
1271 | } |
1272 | } |
1273 | |
1274 | return nullptr; |
1275 | } |
1276 | |
1277 | void QPhysicsWorld::matchOrphanNodes() |
1278 | { |
1279 | // FIXME: does this need thread safety? |
1280 | if (worldManager.orphanNodes.isEmpty()) |
1281 | return; |
1282 | |
1283 | qsizetype numNodes = worldManager.orphanNodes.length(); |
1284 | qsizetype idx = 0; |
1285 | |
1286 | while (idx < numNodes) { |
1287 | auto node = worldManager.orphanNodes[idx]; |
1288 | auto world = getWorld(node); |
1289 | if (world == this) { |
1290 | world->m_newPhysicsNodes.push_back(t: node); |
1291 | // swap-erase |
1292 | worldManager.orphanNodes.swapItemsAt(i: idx, j: numNodes - 1); |
1293 | worldManager.orphanNodes.pop_back(); |
1294 | numNodes--; |
1295 | } else { |
1296 | idx++; |
1297 | } |
1298 | } |
1299 | } |
1300 | |
1301 | void QPhysicsWorld::findPhysicsNodes() |
1302 | { |
1303 | // This method finds the physics nodes inside the scene pointed to by the |
1304 | // scene property. This method is necessary to run whenever the scene |
1305 | // property is changed. |
1306 | if (m_scene == nullptr) |
1307 | return; |
1308 | |
1309 | // Recursively go through all children and add all QAbstractPhysicsNode's |
1310 | QList<QQuick3DObject *> children = m_scene->childItems(); |
1311 | while (!children.empty()) { |
1312 | auto child = children.takeFirst(); |
1313 | if (auto converted = qobject_cast<QAbstractPhysicsNode *>(object: child); converted != nullptr) { |
1314 | // This should never happen but check anyway. |
1315 | if (converted->m_backendObject != nullptr) { |
1316 | qWarning() << "Warning: physics node already associated with a backend node." ; |
1317 | continue; |
1318 | } |
1319 | |
1320 | m_newPhysicsNodes.push_back(t: converted); |
1321 | worldManager.orphanNodes.removeAll(t: converted); // No longer orphan |
1322 | } |
1323 | children.append(l: child->childItems()); |
1324 | } |
1325 | } |
1326 | |
1327 | void QPhysicsWorld::emitContactCallbacks() |
1328 | { |
1329 | for (const QPhysicsWorld::BodyContact &contact : m_registeredContacts) { |
1330 | if (m_removedPhysicsNodes.contains(value: contact.sender) |
1331 | || m_removedPhysicsNodes.contains(value: contact.receiver)) |
1332 | continue; |
1333 | contact.receiver->registerContact(body: contact.sender, positions: contact.positions, impulses: contact.impulses, |
1334 | normals: contact.normals); |
1335 | } |
1336 | |
1337 | m_registeredContacts.clear(); |
1338 | } |
1339 | |
1340 | physx::PxPhysics *QPhysicsWorld::getPhysics() |
1341 | { |
1342 | return StaticPhysXObjects::getReference().physics; |
1343 | } |
1344 | |
1345 | physx::PxCooking *QPhysicsWorld::getCooking() |
1346 | { |
1347 | return StaticPhysXObjects::getReference().cooking; |
1348 | } |
1349 | |
1350 | physx::PxControllerManager *QPhysicsWorld::controllerManager() |
1351 | { |
1352 | if (m_physx->scene && !m_physx->controllerManager) { |
1353 | m_physx->controllerManager = PxCreateControllerManager(scene&: *m_physx->scene); |
1354 | qCDebug(lcQuick3dPhysics) << "Created controller manager" << m_physx->controllerManager; |
1355 | } |
1356 | return m_physx->controllerManager; |
1357 | } |
1358 | |
1359 | QQuick3DNode *QPhysicsWorld::scene() const |
1360 | { |
1361 | return m_scene; |
1362 | } |
1363 | |
1364 | void QPhysicsWorld::setScene(QQuick3DNode *newScene) |
1365 | { |
1366 | if (m_scene == newScene) |
1367 | return; |
1368 | |
1369 | m_scene = newScene; |
1370 | |
1371 | // Delete all nodes since they are associated with the previous scene |
1372 | for (auto body : m_physXBodies) { |
1373 | deregisterNode(physicsNode: body->frontendNode); |
1374 | } |
1375 | |
1376 | // Check if scene is already used by another world |
1377 | bool sceneOK = true; |
1378 | for (QPhysicsWorld *world : worldManager.worlds) { |
1379 | if (world != this && world->scene() == newScene) { |
1380 | sceneOK = false; |
1381 | qWarning() << "Warning: scene already associated with physics world" ; |
1382 | } |
1383 | } |
1384 | |
1385 | if (sceneOK) |
1386 | findPhysicsNodes(); |
1387 | emit sceneChanged(); |
1388 | } |
1389 | |
1390 | int QPhysicsWorld::numThreads() const |
1391 | { |
1392 | return m_numThreads; |
1393 | } |
1394 | |
1395 | void QPhysicsWorld::setNumThreads(int newNumThreads) |
1396 | { |
1397 | if (m_numThreads == newNumThreads) |
1398 | return; |
1399 | m_numThreads = newNumThreads; |
1400 | emit numThreadsChanged(); |
1401 | } |
1402 | |
1403 | bool QPhysicsWorld::reportKinematicKinematicCollisions() const |
1404 | { |
1405 | return m_reportKinematicKinematicCollisions; |
1406 | } |
1407 | |
1408 | void QPhysicsWorld::setReportKinematicKinematicCollisions( |
1409 | bool newReportKinematicKinematicCollisions) |
1410 | { |
1411 | if (m_reportKinematicKinematicCollisions == newReportKinematicKinematicCollisions) |
1412 | return; |
1413 | m_reportKinematicKinematicCollisions = newReportKinematicKinematicCollisions; |
1414 | emit reportKinematicKinematicCollisionsChanged(); |
1415 | } |
1416 | |
1417 | bool QPhysicsWorld::reportStaticKinematicCollisions() const |
1418 | { |
1419 | return m_reportStaticKinematicCollisions; |
1420 | } |
1421 | |
1422 | void QPhysicsWorld::setReportStaticKinematicCollisions(bool newReportStaticKinematicCollisions) |
1423 | { |
1424 | if (m_reportStaticKinematicCollisions == newReportStaticKinematicCollisions) |
1425 | return; |
1426 | m_reportStaticKinematicCollisions = newReportStaticKinematicCollisions; |
1427 | emit reportStaticKinematicCollisionsChanged(); |
1428 | } |
1429 | |
1430 | QT_END_NAMESPACE |
1431 | |
1432 | #include "qphysicsworld.moc" |
1433 | |