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
qphysicsworld.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 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
7#include "physxnode/qabstractphysxnode_p.h"
8#include "physxnode/qphysxworld_p.h"
12#include "joints/qjoint_p.h"
14#include "qboxshape_p.h"
15#include "qsphereshape_p.h"
20#include "qplaneshape_p.h"
22
23#include "PxPhysicsAPI.h"
24#include "cooking/PxCooking.h"
25#include <foundation/PxSimpleTypes.h>
26
27#include <QtQuick/private/qquickframeanimation_p.h>
28#include <QtQuick3D/private/qquick3dobject_p.h>
29#include <QtQuick3D/private/qquick3dnode_p.h>
30#include <QtQuick3D/private/qquick3dmodel_p.h>
31#include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>
32#include <QtQuick3DUtils/private/qssgutils_p.h>
33
34#include <QtGui/qquaternion.h>
35
36#include <QtEnvironmentVariables>
37
38#define PHYSX_ENABLE_PVD 0
39
41
42/*!
43 \qmltype PhysicsWorld
44 \inqmlmodule QtQuick3D.Physics
45 \since 6.4
46 \brief Controls the physics simulation.
47
48 The PhysicsWorld type controls the physics simulation. This node is used to create an instance of the physics world as well
49 as define its properties. There can only be one physics world. All collision nodes in the qml
50 will get added automatically to the physics world.
51*/
52
53/*!
54 \qmlproperty vector3d PhysicsWorld::gravity
55 This property defines the gravity vector of the physics world.
56 The default value is \c (0, -981, 0). Set the value to \c{Qt.vector3d(0, -9.81, 0)} if your
57 unit of measurement is meters and you are simulating Earth gravity.
58*/
59
60/*!
61 \qmlproperty bool PhysicsWorld::running
62 This property starts or stops the physical simulation. The default value is \c true.
63*/
64
65/*!
66 \qmlproperty bool PhysicsWorld::forceDebugDraw
67 This property enables debug drawing of all active shapes in the physics world. The default value
68 is \c false.
69*/
70
71/*!
72 \qmlproperty bool PhysicsWorld::enableCCD
73 \deprecated [6.13] Use DynamicRigidBody::ccd instead.
74
75 This property enables continuous collision detection globally for every dynamic body
76 in the scene that does not explicitly set its own \l DynamicRigidBody::ccd mode.
77 It reduces the risk of fast-moving bodies passing through geometry at high velocities
78 (also known as tunnelling).
79
80 For non-kinematic dynamic bodies, enabling this property uses sweep-based CCD.
81 For kinematic bodies, it automatically uses speculative CCD, as sweep-based CCD
82 is not supported for them.
83
84 Continuous collision detection can also be configured per body via
85 \l DynamicRigidBody::ccd, which avoids paying its performance cost for
86 objects that do not need it.
87
88 \warning Using trigger bodies with CCD enabled is not supported and can
89 result in missing or false trigger reports.
90
91 \default false
92
93 \sa DynamicRigidBody::ccd
94*/
95
96/*!
97 \qmlproperty real PhysicsWorld::typicalLength
98 This property defines the approximate size of objects in the simulation. This is used to
99 estimate certain length-related tolerances. Objects much smaller or much larger than this
100 size may not behave properly. The default value is \c 100.
101
102 Range: \c{[0, inf]}
103*/
104
105/*!
106 \qmlproperty real PhysicsWorld::typicalSpeed
107 This property defines the typical magnitude of velocities of objects in simulation. This is used
108 to estimate whether a contact should be treated as bouncing or resting based on its impact
109 velocity, and a kinetic energy threshold below which the simulation may put objects to sleep.
110
111 For normal physical environments, a good choice is the approximate speed of an object falling
112 under gravity for one second. The default value is \c 1000.
113
114 Range: \c{[0, inf]}
115*/
116
117/*!
118 \qmlproperty real PhysicsWorld::defaultDensity
119 This property defines the default density of dynamic objects, measured in kilograms per cubic
120 unit. This is equal to the weight of a cube with side \c 1.
121
122 The default value is \c 0.001, corresponding to 1 g/cm³: the density of water. If your unit of
123 measurement is meters, a good value would be \c 1000. Note that only positive values are
124 allowed.
125
126 Range: \c{(0, inf]}
127*/
128
129/*!
130 \qmlproperty Node PhysicsWorld::viewport
131 This property defines the viewport where debug components will be drawn if \l{forceDebugDraw}
132 is enabled. If unset the \l{scene} node will be used.
133
134 \sa forceDebugDraw, scene
135*/
136
137/*!
138 \qmlproperty real PhysicsWorld::minimumTimestep
139 This property defines the minimum simulation timestep in milliseconds. The default value is
140 \c{1}.
141
142 Range: \c{[0, maximumTimestep]}
143
144 \note The simulation timestep works in lockstep with the rendering,
145 meaning a new simulation frame will only be started after a rendered frame
146 has completed. This means that at most one simulation frame will run per
147 rendered frame.
148*/
149
150/*!
151 \qmlproperty real PhysicsWorld::maximumTimestep
152 This property defines the maximum simulation timestep in milliseconds. The default value is
153 \c{33.333}.
154
155 Range: \c{[0, inf]}
156
157 \note The simulation timestep works in lockstep with the rendering,
158 meaning a new simulation frame will only be started after a rendered frame
159 has completed. This means that at most one simulation frame will run per
160 rendered frame.
161*/
162
163/*!
164 \qmlproperty Node PhysicsWorld::scene
165
166 This property defines the top-most Node that contains all the nodes of the physical
167 simulation. All physics objects that are an ancestor of this node will be seen as part of this
168 PhysicsWorld.
169
170 \note Using the same scene node for several PhysicsWorld is unsupported.
171*/
172
173/*!
174 \qmlsignal PhysicsWorld::frameDone(float timestep)
175 \since 6.5
176
177 This signal is emitted when the physical simulation is done simulating a frame. The \a timestep
178 parameter is how long in milliseconds the timestep was in the simulation.
179*/
180
181/*!
182 \qmlproperty int PhysicsWorld::numThreads
183 \since 6.7
184
185 This property defines the number of threads used for the physical simulation. This is how the
186 range of values are interpreted:
187
188 \table
189 \header
190 \li Value
191 \li Range
192 \li Description
193 \row
194 \li Negative
195 \li \c{[-inf, -1]}
196 \li Automatic thread count. The application will try to query the number of threads from the
197 system.
198 \row
199 \li Zero
200 \li \c{{0}}
201 \li No threading, simulation will run sequentially.
202 \row
203 \li Positive
204 \li \c{[1, 256]}
205 \li Specific thread count. Values above 256 are clamped.
206 \endtable
207
208 The default value is \c{-1}, meaning automatic thread count.
209
210 \note Once the scene has started running it is not possible to change the number of threads.
211*/
212
213/*!
214 \qmlproperty bool PhysicsWorld::reportKinematicKinematicCollisions
215 \since 6.7
216
217 This property controls if collisions between pairs of \e{kinematic} dynamic rigid bodies will
218 trigger a contact report.
219
220 The default value is \c{false}.
221
222 \note Once the scene has started running it is not possible to change this setting.
223 \sa PhysicsWorld::reportStaticKinematicCollisions
224 \sa DynamicRigidBody
225 \sa PhysicsNode::bodyContact
226*/
227
228/*!
229 \qmlproperty bool PhysicsWorld::reportStaticKinematicCollisions
230 \since 6.7
231
232 This property controls if collisions between a static rigid body and a \e{kinematic} dynamic
233 rigid body will trigger a contact report.
234
235 The default value is \c{false}.
236
237 \note Once the scene has started running it is not possible to change this setting.
238 \sa PhysicsWorld::reportKinematicKinematicCollisions
239 \sa StaticRigidBody
240 \sa DynamicRigidBody
241 \sa PhysicsNode::bodyContact
242*/
243
244/*!
245 \qmlproperty QueryStructure PhysicsWorld::staticQueryStructure
246 \since 6.13
247
248 The spatial pruning structure type used to accelerate scene queries
249 (raycasts, sweeps, overlaps - used in CharacterController and a \e{kinematic} dynamic rigid body)
250 against static actors in the physics scene. It has no effect on rigid-body
251 contact/collision detection during simulation.
252
253 The following values are available:
254
255 \value PhysicsWorld.StaticTree
256 Uses a pre-baked static AABB tree. Offers maximum scene query performance with zero per-frame management overhead.
257 Best for fully static, immutable scenes. Inserting or removing objects at runtime causes heavy scene rebuilds and frame spikes.
258 \value PhysicsWorld.DynamicTree
259 Uses a dynamic self-balancing AABB tree. Allows fast, local runtime insertion (logarithmic time) and removal of static objects without freezing the frame.
260 Ideal for seamless open-world streaming where static chunks are loaded dynamically.
261
262 \default PhysicsWorld.DynamicTree
263
264 \note Once the scene has started running it is not possible to change this setting.
265 \note PhysicsWorld.NoStructure is not supported for staticQueryStructure
266
267 \sa PhysicsWorld::dynamicQueryStructure
268
269*/
270
271/*!
272 \qmlproperty QueryStructure PhysicsWorld::dynamicQueryStructure
273 \since 6.13
274
275 The spatial pruning structure type used to accelerate scene queries
276 (raycasts, sweeps, overlaps - used in CharacterController and a \e{kinematic} dynamic rigid body)
277 against dynamic actors in the physics scene. It has no effect on rigid-body
278 contact/collision detection during simulation.
279
280 The following values are available:
281
282 \value PhysicsWorld.NoStructure
283 Disables the scene query acceleration structure for dynamic actors.
284 Eliminates CPU overhead for tree maintenance when objects move, spawn, or are destroyed.
285 Scene queries will fall back to a linear search. Ideal when scene queries are not needed for dynamic objects.
286 \value PhysicsWorld.StaticTree
287 Uses a static AABB tree. Offers faster scene queries for dynamic actors, but updating the tree when objects move or spawn is very expensive.
288 Best when dynamic actors rarely move or spend most of their time sleeping.
289 \value PhysicsWorld.DynamicTree
290 Uses a dynamic self-balancing AABB tree. Allows fast, local runtime insertion (logarithmic time), movement, and removal of dynamic objects without freezing the frame.
291 Ideal for active scenes with frequently moving or spawning dynamic actors.
292
293 \default PhysicsWorld.DynamicTree
294
295 \note Once the scene has started running it is not possible to change this setting.
296 \sa PhysicsWorld::staticQueryStructure
297*/
298
299Q_LOGGING_CATEGORY(lcQuick3dPhysics, "qt.quick3d.physics");
300
301// Setting QT_PHYSICS_TIMINGS_FILE to a filepath will generate a csv file with frame timings.
302// Note that if running several PhysicsWorld's the last one to be destructed will overwrite
303// the output file.
304//
305// To view it in gnuplot, run these two commands:
306//
307// set datafile separator ','
308// plot '<QT_PHYSICS_TIMINGS_FILE>' using 1:2 with lines
309//
310// Security note: This file is trusted since it is opened as write-only
311static const QString qtPhysicsTimingsFile = qEnvironmentVariable("QT_PHYSICS_TIMINGS_FILE");
312
313/////////////////////////////////////////////////////////////////////////////
314
316{
318public:
320 {
321 // Needed to start the frame animation
322 classBegin();
324 }
325};
326
327/////////////////////////////////////////////////////////////////////////////
328
329void QPhysicsWorld::DebugModelHolder::releaseMeshPointer()
330{
331 if (auto base = static_cast<physx::PxBase *>(ptr); base)
332 base->release();
333 ptr = nullptr;
334}
335
336const QVector3D &QPhysicsWorld::DebugModelHolder::halfExtents() const
337{
338 return data;
339}
340void QPhysicsWorld::DebugModelHolder::setHalfExtents(const QVector3D &halfExtents)
341{
342 data = halfExtents;
343}
344float QPhysicsWorld::DebugModelHolder::radius() const
345{
346 return data.x();
347}
348void QPhysicsWorld::DebugModelHolder::setRadius(float radius)
349{
350 data.setX(radius);
351}
352float QPhysicsWorld::DebugModelHolder::heightScale() const
353{
354 return data.x();
355}
356void QPhysicsWorld::DebugModelHolder::setHeightScale(float heightScale)
357{
358 data.setX(heightScale);
359}
360float QPhysicsWorld::DebugModelHolder::halfHeight() const
361{
362 return data.y();
363}
364void QPhysicsWorld::DebugModelHolder::setHalfHeight(float halfHeight)
365{
366 data.setY(halfHeight);
367}
368float QPhysicsWorld::DebugModelHolder::rowScale() const
369{
370 return data.y();
371}
372void QPhysicsWorld::DebugModelHolder::setRowScale(float rowScale)
373{
374 data.setY(rowScale);
375}
376float QPhysicsWorld::DebugModelHolder::columnScale() const
377{
378 return data.z();
379}
380void QPhysicsWorld::DebugModelHolder::setColumnScale(float columnScale)
381{
382 data.setZ(columnScale);
383}
384physx::PxConvexMesh *QPhysicsWorld::DebugModelHolder::getConvexMesh()
385{
386 return static_cast<physx::PxConvexMesh *>(ptr);
387}
388void QPhysicsWorld::DebugModelHolder::setConvexMesh(physx::PxConvexMesh *mesh)
389{
390 ptr = static_cast<void *>(mesh);
391}
392physx::PxTriangleMesh *QPhysicsWorld::DebugModelHolder::getTriangleMesh()
393{
394 return static_cast<physx::PxTriangleMesh *>(ptr);
395}
396void QPhysicsWorld::DebugModelHolder::setTriangleMesh(physx::PxTriangleMesh *mesh)
397{
398 ptr = static_cast<void *>(mesh);
399}
400physx::PxHeightField *QPhysicsWorld::DebugModelHolder::getHeightField()
401{
402 return static_cast<physx::PxHeightField *>(ptr);
403}
404void QPhysicsWorld::DebugModelHolder::setHeightField(physx::PxHeightField *hf)
405{
406 ptr = static_cast<physx::PxHeightField *>(hf);
407}
408
409/////////////////////////////////////////////////////////////////////////////
410
417
418static QWorldManager worldManager = QWorldManager {};
419
420void QPhysicsWorld::registerNode(QAbstractPhysicsNode *physicsNode)
421{
422 auto world = getWorld(physicsNode);
423 if (world) {
424 world->m_newPhysicsNodes.push_back(physicsNode);
425 } else {
426 worldManager.orphanNodes.push_back(physicsNode);
427 }
428}
429
430void QPhysicsWorld::deregisterNode(QAbstractPhysicsNode *physicsNode)
431{
432 for (auto world : std::as_const(worldManager.worlds)) {
433 world->m_newPhysicsNodes.removeAll(physicsNode);
434 QMutexLocker locker(&world->m_removedPhysicsNodesMutex);
435 if (physicsNode->m_backendObject) {
436 Q_ASSERT(physicsNode->m_backendObject->frontendNode == physicsNode);
437 physicsNode->m_backendObject->frontendNode = nullptr;
438 physicsNode->m_backendObject->isRemoved = true;
439 physicsNode->m_backendObject = nullptr;
440 }
441 world->m_removedPhysicsNodes.insert(physicsNode);
442 }
443 worldManager.orphanNodes.removeAll(physicsNode);
444}
445
446void QPhysicsWorld::registerJoint(QPhysicsJoint *joint)
447{
448 auto world = getWorld(joint);
449 if (world) {
450 world->m_joints.push_back(joint);
451 } else {
452 worldManager.orphanJoints.push_back(joint);
453 }
454}
455
456void QPhysicsWorld::deregisterJoint(QPhysicsJoint *joint)
457{
458 for (auto world : worldManager.worlds) {
459 QMutexLocker locker(&world->m_removedPhysicsNodesMutex);
460 world->m_removedJoints.insert(joint->getPhysXBackend());
461
462 // Swap erase since order does not matter
463 qsizetype idx = world->m_joints.indexOf(joint);
464 if (idx != -1) {
465 world->m_joints.swapItemsAt(idx, world->m_joints.size() - 1);
466 world->m_joints.pop_back();
467 }
468 }
469 worldManager.orphanJoints.removeAll(joint);
470}
471
472void QPhysicsWorld::registerContact(QAbstractPhysicsNode *sender, QAbstractPhysicsNode *receiver,
473 const QVector<QVector3D> &positions,
474 const QVector<QVector3D> &impulses,
475 const QVector<QVector3D> &normals)
476{
477 // Since collision callbacks happen in the physx simulation thread we need
478 // to store these callbacks. Otherwise, if an object is deleted in the same
479 // frame a 'onBodyContact' signal is enqueued and a crash will happen.
480 // Therefore we save these contact callbacks and run them at the end of the
481 // physics frame when we know if the objects are deleted or not.
482
483 BodyContact contact;
484 contact.sender = sender;
485 contact.receiver = receiver;
486 contact.positions = positions;
487 contact.impulses = impulses;
488 contact.normals = normals;
489
490 m_registeredContacts.push_back(contact);
491}
492
493QPhysicsWorld::QPhysicsWorld(QObject *parent) : QObject(parent)
494{
495 m_inDesignStudio = !qEnvironmentVariableIsEmpty("QML_PUPPET_MODE");
496 m_physx = new QPhysXWorld;
497 m_physx->createWorld();
498
499 worldManager.worlds.push_back(this);
500 matchOrphanNodes();
501 matchOrphanJoints();
502
503 m_frameAnimator = new FrameAnimator;
504 connect(m_frameAnimator, &QQuickFrameAnimation::triggered, this,
505 &QPhysicsWorld::simulateFrame);
506}
507
508QPhysicsWorld::~QPhysicsWorld()
509{
510 if (m_frameAnimator) {
511 m_frameAnimator->stop();
512 delete m_frameAnimator;
513 }
514
515 if (m_physx->scene && m_physx->isRunning && !m_frameFetched)
516 m_physx->scene->fetchResults(true);
517
518 for (auto body : std::as_const(m_physXBodies)) {
519 body->cleanup(m_physx);
520 delete body;
521 }
522 m_physXBodies.clear();
523 m_physx->deleteWorld();
524 delete m_physx;
525 worldManager.worlds.removeAll(this);
526
527 if (!qtPhysicsTimingsFile.isEmpty()) {
528 if (m_frameTimings.isEmpty()) {
529 qWarning() << "No frame timings saved.";
530 } else if (auto csvFile = QFile(qtPhysicsTimingsFile); csvFile.open(QIODevice::WriteOnly)) {
531 QTextStream out(&csvFile);
532 for (int i = 1; i < m_frameTimings.size(); i++) {
533 out << i << "," << m_frameTimings[i] << '\n';
534 }
535 csvFile.close();
536 } else {
537 qWarning() << "Could not open timings file " << qtPhysicsTimingsFile;
538 }
539 }
540}
541
542void QPhysicsWorld::classBegin() {}
543
544void QPhysicsWorld::componentComplete()
545{
546 if ((!m_running && !m_inDesignStudio) || m_physicsInitialized)
547 return;
548 initPhysics();
549}
550
551QVector3D QPhysicsWorld::gravity() const
552{
553 return m_gravity;
554}
555
556bool QPhysicsWorld::running() const
557{
558 return m_running;
559}
560
561bool QPhysicsWorld::forceDebugDraw() const
562{
563 return m_forceDebugDraw;
564}
565
566bool QPhysicsWorld::enableCCD() const
567{
568 return m_enableCCD;
569}
570
571float QPhysicsWorld::typicalLength() const
572{
573 return m_typicalLength;
574}
575
576float QPhysicsWorld::typicalSpeed() const
577{
578 return m_typicalSpeed;
579}
580
581bool QPhysicsWorld::isNodeRemoved(QAbstractPhysicsNode *object)
582{
583 return m_removedPhysicsNodes.contains(object);
584}
585
586void QPhysicsWorld::setGravity(QVector3D gravity)
587{
588 if (m_gravity == gravity)
589 return;
590
591 if (!QPhysicsUtils::isFinite(gravity)) {
592 qWarning() << "Warning: 'gravity' must be finite, ignored";
593 return;
594 }
595
596 m_gravity = gravity;
597 if (m_physx->scene) {
598 m_physx->scene->setGravity(QPhysicsUtils::toPhysXType(m_gravity));
599 }
600 emit gravityChanged(m_gravity);
601}
602
603void QPhysicsWorld::setRunning(bool running)
604{
605 if (m_running == running)
606 return;
607
608 m_running = running;
609 if (!m_inDesignStudio && m_running && !m_physicsInitialized)
610 initPhysics();
611
612 if (running)
613 m_frameAnimator->start();
614 else
615 m_frameAnimator->stop();
616
617 emit runningChanged(m_running);
618}
619
620void QPhysicsWorld::setForceDebugDraw(bool forceDebugDraw)
621{
622 if (m_forceDebugDraw == forceDebugDraw)
623 return;
624
625 m_forceDebugDraw = forceDebugDraw;
626 if (!m_forceDebugDraw)
627 disableDebugDraw();
628 else
629 updateDebugDraw();
630 emit forceDebugDrawChanged(m_forceDebugDraw);
631}
632
633QQuick3DNode *QPhysicsWorld::viewport() const
634{
635 return m_viewport;
636}
637
638void QPhysicsWorld::setHasIndividualDebugDraw()
639{
640 m_hasIndividualDebugDraw = true;
641}
642
643void QPhysicsWorld::setViewport(QQuick3DNode *viewport)
644{
645 if (m_viewport == viewport)
646 return;
647
648 m_viewport = viewport;
649
650 // TODO: test this
651 for (auto material : std::as_const(m_debugMaterials))
652 delete material;
653 m_debugMaterials.clear();
654
655 for (auto &holder : m_collisionShapeDebugModels) {
656 holder.releaseMeshPointer();
657 delete holder.model;
658 }
659 m_collisionShapeDebugModels.clear();
660
661 emit viewportChanged(m_viewport);
662}
663
664void QPhysicsWorld::setMinimumTimestep(float minTimestep)
665{
666 if (qFuzzyCompare(m_minTimestep, minTimestep))
667 return;
668
669 if (!qIsFinite(minTimestep)) {
670 qWarning("Minimum timestep must be finite, ignoring");
671 return;
672 }
673
674 if (minTimestep > m_maxTimestep) {
675 qWarning("Minimum timestep greater than maximum timestep, value clamped");
676 minTimestep = qMin(minTimestep, m_maxTimestep);
677 }
678
679 if (minTimestep < 0.f) {
680 qWarning("Minimum timestep less than zero, value clamped");
681 minTimestep = qMax(minTimestep, 0.f);
682 }
683
684 if (qFuzzyCompare(m_minTimestep, minTimestep))
685 return;
686
687 m_minTimestep = minTimestep;
688 emit minimumTimestepChanged(m_minTimestep);
689}
690
691void QPhysicsWorld::setMaximumTimestep(float maxTimestep)
692{
693 if (qFuzzyCompare(m_maxTimestep, maxTimestep))
694 return;
695
696 if (!qIsFinite(maxTimestep)) {
697 qWarning("Maximum timestep must be finite, ignoring");
698 return;
699 }
700
701 if (maxTimestep < 0.f) {
702 qWarning("Maximum timestep less than zero, value clamped");
703 maxTimestep = qMax(maxTimestep, 0.f);
704 }
705
706 if (qFuzzyCompare(m_maxTimestep, maxTimestep))
707 return;
708
709 m_maxTimestep = maxTimestep;
710 emit maximumTimestepChanged(maxTimestep);
711}
712
713void QPhysicsWorld::setupDebugMaterials(QQuick3DNode *sceneNode)
714{
715 if (!m_debugMaterials.isEmpty())
716 return;
717
718 const int lineWidth = m_inDesignStudio ? 1 : 3;
719
720 // These colors match the indices of DebugDrawBodyType enum
721 for (auto color : { QColorConstants::Svg::chartreuse, QColorConstants::Svg::cyan,
722 QColorConstants::Svg::lightsalmon, QColorConstants::Svg::red,
723 QColorConstants::Svg::blueviolet, QColorConstants::Svg::black }) {
724 auto debugMaterial = new QQuick3DPrincipledMaterial();
725 debugMaterial->setLineWidth(lineWidth);
726 debugMaterial->setParentItem(sceneNode);
727 debugMaterial->setParent(sceneNode);
728 debugMaterial->setBaseColor(color);
729 debugMaterial->setLighting(QQuick3DPrincipledMaterial::NoLighting);
730 debugMaterial->setCullMode(QQuick3DMaterial::NoCulling);
731 m_debugMaterials.push_back(debugMaterial);
732 }
733}
734
735void QPhysicsWorld::updateDebugDraw()
736{
737 if (!(m_forceDebugDraw || m_hasIndividualDebugDraw)) {
738 // Nothing to draw, trash all previous models (if any) and return
739 for (auto &holder : m_collisionShapeDebugModels) {
740 holder.releaseMeshPointer();
741 delete holder.model;
742 }
743 m_collisionShapeDebugModels.clear();
744 return;
745 }
746
747 // Use scene node if no viewport has been specified
748 auto sceneNode = m_viewport ? m_viewport : m_scene;
749
750 if (sceneNode == nullptr)
751 return;
752
753 setupDebugMaterials(sceneNode);
754 m_hasIndividualDebugDraw = false;
755
756 // Store the collision shapes we have now so we can clear out the removed ones
757 QSet<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>> currentCollisionShapes;
758 currentCollisionShapes.reserve(m_collisionShapeDebugModels.size());
759
760 for (QAbstractPhysXNode *node : std::as_const(m_physXBodies)) {
761 if (!node->debugGeometryCapability())
762 continue;
763
764 const auto &collisionShapes = node->frontendNode->getCollisionShapesList();
765 const int materialIdx = static_cast<int>(node->getDebugDrawBodyType());
766 const int length = collisionShapes.length();
767 for (int idx = 0; idx < length; idx++) {
768 const auto collisionShape = collisionShapes[idx];
769
770 if (!m_forceDebugDraw && !collisionShape->enableDebugDraw())
771 continue;
772
773 DebugModelHolder &holder =
774 m_collisionShapeDebugModels[std::make_pair(collisionShape, node)];
775 auto &model = holder.model;
776
777 currentCollisionShapes.insert(std::make_pair(collisionShape, node));
778
779 m_hasIndividualDebugDraw =
780 m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
781
782 // Create/Update debug view infrastructure
783 if (!model) {
784 model = new QQuick3DModel();
785 model->setParentItem(sceneNode);
786 model->setParent(sceneNode);
787 model->setCastsShadows(false);
788 model->setReceivesShadows(false);
789 model->setCastsReflections(false);
790 }
791
792 model->setVisible(true);
793
794 { // update or set material
795 auto material = m_debugMaterials[materialIdx];
796 QQmlListReference materialsRef(model, "materials");
797 if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
798 materialsRef.clear();
799 materialsRef.append(material);
800 }
801 }
802
803 // Special handling of CharacterController since it has collision shapes,
804 // but not PhysX shapes
805 if (qobject_cast<QCharacterController *>(node->frontendNode)) {
806 QCapsuleShape *capsuleShape = qobject_cast<QCapsuleShape *>(collisionShape);
807 if (!capsuleShape)
808 continue;
809
810 const float radius = capsuleShape->diameter() * 0.5;
811 const float halfHeight = capsuleShape->height() * 0.5;
812
813 if (!qFuzzyCompare(radius, holder.radius())
814 || !qFuzzyCompare(halfHeight, holder.halfHeight())) {
815 auto geom = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
816 geom->setParent(model);
817 model->setGeometry(geom);
818 holder.setRadius(radius);
819 holder.setHalfHeight(halfHeight);
820 }
821
822 model->setPosition(node->frontendNode->scenePosition());
823 model->setRotation(node->frontendNode->sceneRotation()
824 * QQuaternion::fromEulerAngles(0, 0, 90));
825 continue;
826 }
827
828 if (node->shapes.length() < length)
829 continue;
830
831 const auto physXShape = node->shapes[idx];
832 auto localPose = physXShape->getLocalPose();
833
834 switch (physXShape->getGeometryType()) {
835 case physx::PxGeometryType::eBOX: {
836 physx::PxBoxGeometry boxGeometry;
837 physXShape->getBoxGeometry(boxGeometry);
838 const auto &halfExtentsOld = holder.halfExtents();
839 const auto halfExtents = QPhysicsUtils::toQtType(boxGeometry.halfExtents);
840 if (!qFuzzyCompare(halfExtentsOld, halfExtents)) {
841 auto geom = QDebugDrawHelper::generateBoxGeometry(halfExtents);
842 geom->setParent(model);
843 model->setGeometry(geom);
844 holder.setHalfExtents(halfExtents);
845 }
846
847 }
848 break;
849
850 case physx::PxGeometryType::eSPHERE: {
851 physx::PxSphereGeometry sphereGeometry;
852 physXShape->getSphereGeometry(sphereGeometry);
853 const float radius = holder.radius();
854 if (!qFuzzyCompare(sphereGeometry.radius, radius)) {
855 auto geom = QDebugDrawHelper::generateSphereGeometry(sphereGeometry.radius);
856 geom->setParent(model);
857 model->setGeometry(geom);
858 holder.setRadius(sphereGeometry.radius);
859 }
860 }
861 break;
862
863 case physx::PxGeometryType::eCAPSULE: {
864 physx::PxCapsuleGeometry capsuleGeometry;
865 physXShape->getCapsuleGeometry(capsuleGeometry);
866 const float radius = holder.radius();
867 const float halfHeight = holder.halfHeight();
868
869 if (!qFuzzyCompare(capsuleGeometry.radius, radius)
870 || !qFuzzyCompare(capsuleGeometry.halfHeight, halfHeight)) {
871 auto geom = QDebugDrawHelper::generateCapsuleGeometry(
872 capsuleGeometry.radius, capsuleGeometry.halfHeight);
873 geom->setParent(model);
874 model->setGeometry(geom);
875 holder.setRadius(capsuleGeometry.radius);
876 holder.setHalfHeight(capsuleGeometry.halfHeight);
877 }
878 }
879 break;
880
881 case physx::PxGeometryType::ePLANE:{
882 physx::PxPlaneGeometry planeGeometry;
883 physXShape->getPlaneGeometry(planeGeometry);
884 // Special rotation
885 const QQuaternion rotation =
886 QPhysicsUtils::kMinus90YawRotation * QPhysicsUtils::toQtType(localPose.q);
887 localPose = physx::PxTransform(localPose.p, QPhysicsUtils::toPhysXType(rotation));
888
889 if (model->geometry() == nullptr) {
890 auto geom = QDebugDrawHelper::generatePlaneGeometry();
891 geom->setParent(model);
892 model->setGeometry(geom);
893 }
894 }
895 break;
896
897 // For heightfield, convex mesh and triangle mesh we increase its reference count
898 // to make sure it does not get dereferenced and deleted so that the new mesh will
899 // have another memory address so we know when it has changed.
900 case physx::PxGeometryType::eHEIGHTFIELD: {
901 physx::PxHeightFieldGeometry heightFieldGeometry;
902 bool success = physXShape->getHeightFieldGeometry(heightFieldGeometry);
903 Q_ASSERT(success);
904 const float heightScale = holder.heightScale();
905 const float rowScale = holder.rowScale();
906 const float columnScale = holder.columnScale();
907
908 if (auto heightField = holder.getHeightField();
909 heightField && heightField != heightFieldGeometry.heightField) {
910 heightField->release();
911 holder.setHeightField(nullptr);
912 }
913
914 if (!qFuzzyCompare(heightFieldGeometry.heightScale, heightScale)
915 || !qFuzzyCompare(heightFieldGeometry.rowScale, rowScale)
916 || !qFuzzyCompare(heightFieldGeometry.columnScale, columnScale)
917 || !holder.getHeightField()) {
918 if (!holder.getHeightField()) {
919 heightFieldGeometry.heightField->acquireReference();
920 holder.setHeightField(heightFieldGeometry.heightField);
921 }
922 auto geom = QDebugDrawHelper::generateHeightFieldGeometry(
923 heightFieldGeometry.heightField, heightFieldGeometry.heightScale,
924 heightFieldGeometry.rowScale, heightFieldGeometry.columnScale);
925 geom->setParent(model);
926 model->setGeometry(geom);
927 holder.setHeightScale(heightFieldGeometry.heightScale);
928 holder.setRowScale(heightFieldGeometry.rowScale);
929 holder.setColumnScale(heightFieldGeometry.columnScale);
930 }
931 }
932 break;
933
934 case physx::PxGeometryType::eCONVEXMESH: {
935 physx::PxConvexMeshGeometry convexMeshGeometry;
936 const bool success = physXShape->getConvexMeshGeometry(convexMeshGeometry);
937 Q_ASSERT(success);
938 const auto rotation = convexMeshGeometry.scale.rotation * localPose.q;
939 localPose = physx::PxTransform(localPose.p, rotation);
940 model->setScale(QPhysicsUtils::toQtType(convexMeshGeometry.scale.scale));
941
942 if (auto convexMesh = holder.getConvexMesh();
943 convexMesh && convexMesh != convexMeshGeometry.convexMesh) {
944 convexMesh->release();
945 holder.setConvexMesh(nullptr);
946 }
947
948 if (!model->geometry() || !holder.getConvexMesh()) {
949 if (!holder.getConvexMesh()) {
950 convexMeshGeometry.convexMesh->acquireReference();
951 holder.setConvexMesh(convexMeshGeometry.convexMesh);
952 }
953 auto geom = QDebugDrawHelper::generateConvexMeshGeometry(
954 convexMeshGeometry.convexMesh);
955 geom->setParent(model);
956 model->setGeometry(geom);
957 }
958 }
959 break;
960
961 case physx::PxGeometryType::eTRIANGLEMESH: {
962 physx::PxTriangleMeshGeometry triangleMeshGeometry;
963 const bool success = physXShape->getTriangleMeshGeometry(triangleMeshGeometry);
964 Q_ASSERT(success);
965 const auto rotation = triangleMeshGeometry.scale.rotation * localPose.q;
966 localPose = physx::PxTransform(localPose.p, rotation);
967 model->setScale(QPhysicsUtils::toQtType(triangleMeshGeometry.scale.scale));
968
969 if (auto triangleMesh = holder.getTriangleMesh();
970 triangleMesh && triangleMesh != triangleMeshGeometry.triangleMesh) {
971 triangleMesh->release();
972 holder.setTriangleMesh(nullptr);
973 }
974
975 if (!model->geometry() || !holder.getTriangleMesh()) {
976 if (!holder.getTriangleMesh()) {
977 triangleMeshGeometry.triangleMesh->acquireReference();
978 holder.setTriangleMesh(triangleMeshGeometry.triangleMesh);
979 }
980 auto geom = QDebugDrawHelper::generateTriangleMeshGeometry(
981 triangleMeshGeometry.triangleMesh);
982 geom->setParent(model);
983 model->setGeometry(geom);
984 }
985 }
986 break;
987
988 case physx::PxGeometryType::eINVALID:
989 case physx::PxGeometryType::eGEOMETRY_COUNT:
990 // should not happen
991 Q_UNREACHABLE();
992 }
993
994 auto globalPose = node->getGlobalPose();
995 auto finalPose = globalPose.transform(localPose);
996
997 model->setRotation(QPhysicsUtils::toQtType(finalPose.q));
998 model->setPosition(QPhysicsUtils::toQtType(finalPose.p));
999 }
1000 }
1001
1002 // Remove old collision shapes
1003 m_collisionShapeDebugModels.removeIf(
1004 [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>,
1005 DebugModelHolder>::iterator it) {
1006 if (!currentCollisionShapes.contains(it.key())) {
1007 auto holder = it.value();
1008 holder.releaseMeshPointer();
1009 if (holder.model)
1010 delete holder.model;
1011 return true;
1012 }
1013 return false;
1014 });
1015}
1016
1017static void collectPhysicsNodes(QQuick3DObject *node, QList<QAbstractPhysicsNode *> &nodes)
1018{
1019 if (auto shape = qobject_cast<QAbstractPhysicsNode *>(node)) {
1020 nodes.push_back(shape);
1021 return;
1022 }
1023
1024 auto childItems = node->childItems();
1025 for (QQuick3DObject *child : std::as_const(childItems))
1026 collectPhysicsNodes(child, nodes);
1027}
1028
1029void QPhysicsWorld::updateDebugDrawDesignStudio()
1030{
1031 // Use scene node if no viewport has been specified
1032 auto sceneNode = m_viewport ? m_viewport : m_scene;
1033
1034 if (sceneNode == nullptr)
1035 return;
1036
1037 setupDebugMaterials(sceneNode);
1038
1039 // Store the collision shapes we have now so we can clear out the removed ones
1040 QSet<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>> currentCollisionShapes;
1041 currentCollisionShapes.reserve(m_collisionShapeDebugModels.size());
1042
1043 QList<QAbstractPhysicsNode *> activePhysicsNodes;
1044 activePhysicsNodes.reserve(m_collisionShapeDebugModels.size());
1045 collectPhysicsNodes(m_scene, activePhysicsNodes);
1046
1047 for (QAbstractPhysicsNode *node : std::as_const(activePhysicsNodes)) {
1048
1049 const auto &collisionShapes = node->getCollisionShapesList();
1050 const int materialIdx = 0; // Just take first material
1051 const int length = collisionShapes.length();
1052
1053 const bool isCharacterController = qobject_cast<QCharacterController *>(node) != nullptr;
1054
1055 for (int idx = 0; idx < length; idx++) {
1056 QAbstractCollisionShape *collisionShape = collisionShapes[idx];
1057 DebugModelHolder &holder =
1058 m_DesignStudioDebugModels[std::make_pair(collisionShape, node)];
1059 auto &model = holder.model;
1060
1061 currentCollisionShapes.insert(std::make_pair(collisionShape, node));
1062
1063 m_hasIndividualDebugDraw =
1064 m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
1065
1066 // Create/Update debug view infrastructure
1067 {
1068 // Hack: we have to delete the model every frame so it shows up in QDS
1069 // whenever the code is updated, not sure why ¯\_(?)_/¯
1070 delete model;
1071 model = new QQuick3DModel();
1072 model->setParentItem(sceneNode);
1073 model->setParent(sceneNode);
1074 model->setCastsShadows(false);
1075 model->setReceivesShadows(false);
1076 model->setCastsReflections(false);
1077 }
1078
1079 const bool hasGeometry = holder.geometry != nullptr;
1080 QVector3D scenePosition = collisionShape->scenePosition();
1081 QQuaternion sceneRotation = collisionShape->sceneRotation();
1082 QQuick3DGeometry *newGeometry = nullptr;
1083
1084 if (isCharacterController)
1085 sceneRotation = sceneRotation * QQuaternion::fromEulerAngles(QVector3D(0, 0, 90));
1086
1087 { // update or set material
1088 auto material = m_debugMaterials[materialIdx];
1089 QQmlListReference materialsRef(model, "materials");
1090 if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
1091 materialsRef.clear();
1092 materialsRef.append(material);
1093 }
1094 }
1095
1096 if (auto shape = qobject_cast<QBoxShape *>(collisionShape)) {
1097 const auto &halfExtentsOld = holder.halfExtents();
1098 const auto halfExtents = shape->sceneScale() * shape->extents() * 0.5f;
1099 if (!qFuzzyCompare(halfExtentsOld, halfExtents) || !hasGeometry) {
1100 newGeometry = QDebugDrawHelper::generateBoxGeometry(halfExtents);
1101 holder.setHalfExtents(halfExtents);
1102 }
1103 } else if (auto shape = qobject_cast<QSphereShape *>(collisionShape)) {
1104 const float radiusOld = holder.radius();
1105 const float radius = shape->sceneScale().x() * shape->diameter() * 0.5f;
1106 if (!qFuzzyCompare(radiusOld, radius) || !hasGeometry) {
1107 newGeometry = QDebugDrawHelper::generateSphereGeometry(radius);
1108 holder.setRadius(radius);
1109 }
1110 } else if (auto shape = qobject_cast<QCapsuleShape *>(collisionShape)) {
1111 const float radiusOld = holder.radius();
1112 const float halfHeightOld = holder.halfHeight();
1113 const float radius = shape->sceneScale().y() * shape->diameter() * 0.5f;
1114 const float halfHeight = shape->sceneScale().x() * shape->height() * 0.5f;
1115
1116 if ((!qFuzzyCompare(radiusOld, radius) || !qFuzzyCompare(halfHeightOld, halfHeight))
1117 || !hasGeometry) {
1118 newGeometry = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
1119 holder.setRadius(radius);
1120 holder.setHalfHeight(halfHeight);
1121 }
1122 } else if (qobject_cast<QPlaneShape *>(collisionShape)) {
1123 if (!hasGeometry)
1124 newGeometry = QDebugDrawHelper::generatePlaneGeometry();
1125 } else if (auto shape = qobject_cast<QHeightFieldShape *>(collisionShape)) {
1126 physx::PxHeightFieldGeometry *heightFieldGeometry =
1127 static_cast<physx::PxHeightFieldGeometry *>(shape->getPhysXGeometry());
1128 const float heightScale = holder.heightScale();
1129 const float rowScale = holder.rowScale();
1130 const float columnScale = holder.columnScale();
1131 scenePosition += shape->hfOffset();
1132 if (!heightFieldGeometry) {
1133 qWarning() << "Could not get height field";
1134 } else if (!qFuzzyCompare(heightFieldGeometry->heightScale, heightScale)
1135 || !qFuzzyCompare(heightFieldGeometry->rowScale, rowScale)
1136 || !qFuzzyCompare(heightFieldGeometry->columnScale, columnScale)
1137 || !hasGeometry) {
1138 newGeometry = QDebugDrawHelper::generateHeightFieldGeometry(
1139 heightFieldGeometry->heightField, heightFieldGeometry->heightScale,
1140 heightFieldGeometry->rowScale, heightFieldGeometry->columnScale);
1141 holder.setHeightScale(heightFieldGeometry->heightScale);
1142 holder.setRowScale(heightFieldGeometry->rowScale);
1143 holder.setColumnScale(heightFieldGeometry->columnScale);
1144 }
1145 } else if (auto shape = qobject_cast<QConvexMeshShape *>(collisionShape)) {
1146 auto convexMeshGeometry =
1147 static_cast<physx::PxConvexMeshGeometry *>(shape->getPhysXGeometry());
1148 if (!convexMeshGeometry) {
1149 qWarning() << "Could not get convex mesh";
1150 } else {
1151 model->setScale(QPhysicsUtils::toQtType(convexMeshGeometry->scale.scale));
1152
1153 if (!hasGeometry) {
1154 newGeometry = QDebugDrawHelper::generateConvexMeshGeometry(
1155 convexMeshGeometry->convexMesh);
1156 }
1157 }
1158 } else if (auto shape = qobject_cast<QTriangleMeshShape *>(collisionShape)) {
1159 physx::PxTriangleMeshGeometry *triangleMeshGeometry =
1160 static_cast<physx::PxTriangleMeshGeometry *>(shape->getPhysXGeometry());
1161 if (!triangleMeshGeometry) {
1162 qWarning() << "Could not get triangle mesh";
1163 } else {
1164 model->setScale(QPhysicsUtils::toQtType(triangleMeshGeometry->scale.scale));
1165
1166 if (!hasGeometry) {
1167 newGeometry = QDebugDrawHelper::generateTriangleMeshGeometry(
1168 triangleMeshGeometry->triangleMesh);
1169 }
1170 }
1171 }
1172
1173 if (newGeometry) {
1174 delete holder.geometry;
1175 holder.geometry = newGeometry;
1176 }
1177
1178 model->setGeometry(holder.geometry);
1179 model->setVisible(true);
1180
1181 model->setRotation(sceneRotation);
1182 model->setPosition(scenePosition);
1183 }
1184 }
1185
1186 // Remove old debug models
1187 m_DesignStudioDebugModels.removeIf(
1188 [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>,
1189 DebugModelHolder>::iterator it) {
1190 if (!currentCollisionShapes.contains(it.key())) {
1191 auto holder = it.value();
1192 holder.releaseMeshPointer();
1193 if (holder.model) {
1194 delete holder.geometry;
1195 delete holder.model;
1196 }
1197 return true;
1198 }
1199 return false;
1200 });
1201}
1202
1203void QPhysicsWorld::disableDebugDraw()
1204{
1205 m_hasIndividualDebugDraw = false;
1206
1207 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1208 const auto &collisionShapes = body->frontendNode->getCollisionShapesList();
1209 const int length = collisionShapes.length();
1210 for (int idx = 0; idx < length; idx++) {
1211 const auto collisionShape = collisionShapes[idx];
1212 if (collisionShape->enableDebugDraw()) {
1213 m_hasIndividualDebugDraw = true;
1214 return;
1215 }
1216 }
1217 }
1218}
1219
1220void QPhysicsWorld::setEnableCCD(bool enableCCD)
1221{
1222 if (m_enableCCD == enableCCD)
1223 return;
1224
1225 if (m_physicsInitialized) {
1226 qWarning()
1227 << "Warning: Changing 'enableCCD' after physics is initialized will have no effect";
1228 return;
1229 }
1230
1231 m_enableCCD = enableCCD;
1232 emit enableCCDChanged(m_enableCCD);
1233}
1234
1235void QPhysicsWorld::setTypicalLength(float typicalLength)
1236{
1237 if (qFuzzyCompare(typicalLength, m_typicalLength))
1238 return;
1239
1240 if (!qIsFinite(typicalLength) || typicalLength <= 0.f) {
1241 qWarning() << "Warning: 'typicalLength' value less than zero, ignored";
1242 return;
1243 }
1244
1245 if (m_physicsInitialized) {
1246 qWarning() << "Warning: Changing 'typicalLength' after physics is initialized will have "
1247 "no effect";
1248 return;
1249 }
1250
1251 m_typicalLength = typicalLength;
1252
1253 emit typicalLengthChanged(typicalLength);
1254}
1255
1256void QPhysicsWorld::setTypicalSpeed(float typicalSpeed)
1257{
1258 if (qFuzzyCompare(typicalSpeed, m_typicalSpeed))
1259 return;
1260
1261 if (!qIsFinite(typicalSpeed) || typicalSpeed <= 0.f) {
1262 qWarning() << "Warning: 'typicalSpeed' must be finite and greater than zero, ignored";
1263 return;
1264 }
1265
1266 if (m_physicsInitialized) {
1267 qWarning() << "Warning: Changing 'typicalSpeed' after physics is initialized will have "
1268 "no effect";
1269 return;
1270 }
1271
1272 m_typicalSpeed = typicalSpeed;
1273
1274 emit typicalSpeedChanged(typicalSpeed);
1275}
1276
1277float QPhysicsWorld::defaultDensity() const
1278{
1279 return m_defaultDensity;
1280}
1281
1282float QPhysicsWorld::minimumTimestep() const
1283{
1284 return m_minTimestep;
1285}
1286
1287float QPhysicsWorld::maximumTimestep() const
1288{
1289 return m_maxTimestep;
1290}
1291
1292void QPhysicsWorld::setDefaultDensity(float defaultDensity)
1293{
1294 defaultDensity = qBound(0.0000001f, defaultDensity, PX_MAX_F32);
1295
1296 if (qFuzzyCompare(m_defaultDensity, defaultDensity))
1297 return;
1298 m_defaultDensity = defaultDensity;
1299
1300 // Go through all dynamic rigid bodies and update the default density
1301 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies))
1302 body->updateDefaultDensity(m_defaultDensity);
1303
1304 emit defaultDensityChanged(defaultDensity);
1305}
1306
1307// Remove physics world items that no longer exist
1308
1309void QPhysicsWorld::cleanupRemovedNodes()
1310{
1311 m_physXBodies.removeIf([this](QAbstractPhysXNode *body) {
1312 return body->cleanupIfRemoved(m_physx);
1313 });
1314 m_removedPhysicsNodes.clear();
1315}
1316
1317void QPhysicsWorld::cleanupRemovedJoints()
1318{
1319 for (physx::PxJoint *joint : m_removedJoints) {
1320 if (joint)
1321 joint->release();
1322 }
1323 m_removedJoints.clear();
1324}
1325
1326void QPhysicsWorld::initPhysics()
1327{
1328 Q_ASSERT(!m_physicsInitialized);
1329
1330 const unsigned int numThreads = m_numThreads >= 0 ? m_numThreads : qMax(0, QThread::idealThreadCount());
1331 m_physx->createScene(m_typicalLength, m_typicalSpeed, m_gravity, this, numThreads);
1332 m_frameAnimator->start();
1333 m_physicsInitialized = true;
1334}
1335
1336void QPhysicsWorld::simulateFrame()
1337{
1338 constexpr double MILLIONTH = 0.000001;
1339 constexpr double THOUSANDTH = 0.001;
1340
1341 if (m_inDesignStudio) {
1342 frameFinishedDesignStudio();
1343 return;
1344 }
1345
1346 if (!m_physx->isRunning) {
1347 m_timer.start();
1348 m_physx->isRunning = true;
1349 const double minTimestepSecs = m_minTimestep * 0.001;
1350 m_physx->scene->simulate(minTimestepSecs);
1351 m_currTimeStep = minTimestepSecs;
1352 return;
1353 }
1354
1355 // Frame not ready yet
1356 if (!m_frameFetched && !m_physx->scene->checkResults()) {
1357 return;
1358 }
1359
1360 // Frame ready, fetch and finish it
1361 if (!m_frameFetched) {
1362 m_physx->scene->fetchResults(true);
1363 frameFinished(m_currTimeStep);
1364 m_frameFetched = true;
1365 if (Q_UNLIKELY(!qtPhysicsTimingsFile.isEmpty())) {
1366 const double deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
1367 m_frameTimings.append(deltaMS);
1368 }
1369 }
1370
1371 // Assuming: 0 <= minTimestep <= maxTimestep
1372 const double deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
1373 if (deltaMS < m_minTimestep)
1374 return;
1375 const double deltaSecs = qMin<double>(deltaMS, m_maxTimestep) * THOUSANDTH;
1376 m_timer.restart();
1377 m_physx->scene->simulate(deltaSecs);
1378 m_frameFetched = false;
1379 m_currTimeStep = deltaSecs;
1380}
1381
1382void QPhysicsWorld::frameFinished(float deltaTime)
1383{
1384 matchOrphanNodes();
1385 matchOrphanJoints();
1386 emitContactCallbacks();
1387 cleanupRemovedNodes();
1388 cleanupRemovedJoints();
1389
1390 for (auto *node : std::as_const(m_newPhysicsNodes)) {
1391 auto *body = node->createPhysXBackend();
1392 body->init(this, m_physx);
1393 m_physXBodies.push_back(body);
1394 }
1395 m_newPhysicsNodes.clear();
1396
1397 QHash<QQuick3DNode *, QMatrix4x4> transformCache;
1398
1399 // TODO: Use dirty flag/dirty list to avoid redoing things that didn't change
1400 for (auto *physXBody : std::as_const(m_physXBodies)) {
1401 physXBody->markDirtyShapes();
1402 physXBody->rebuildDirtyShapes(this, m_physx);
1403 physXBody->updateFilters();
1404
1405 // Sync the physics world and the scene
1406 physXBody->sync(deltaTime, transformCache);
1407 }
1408
1409 for (QPhysicsJoint *joint : std::as_const(m_joints)) {
1410 joint->updatePhysXBackend();
1411 }
1412
1413 updateDebugDraw();
1414 emit frameDone(deltaTime * 1000);
1415}
1416
1417void QPhysicsWorld::frameFinishedDesignStudio()
1418{
1419 // Note sure if this is needed but do it anyway
1420 matchOrphanNodes();
1421 matchOrphanJoints();
1422 emitContactCallbacks();
1423 cleanupRemovedNodes();
1424 // Ignore new physics nodes, we find them from the scene node anyway
1425 m_newPhysicsNodes.clear();
1426
1427 updateDebugDrawDesignStudio();
1428}
1429
1430QPhysicsWorld *QPhysicsWorld::getWorld(QQuick3DNode *node)
1431{
1432 for (QPhysicsWorld *world : std::as_const(worldManager.worlds)) {
1433 if (!world->m_scene) {
1434 continue;
1435 }
1436
1437 QQuick3DNode *nodeCurr = node;
1438
1439 // Maybe pointless but check starting node
1440 if (nodeCurr == world->m_scene)
1441 return world;
1442
1443 while (nodeCurr->parentNode()) {
1444 nodeCurr = nodeCurr->parentNode();
1445 if (nodeCurr == world->m_scene)
1446 return world;
1447 }
1448 }
1449
1450 return nullptr;
1451}
1452
1453QPhysicsWorld *QPhysicsWorld::getWorld(QPhysicsJoint *joint)
1454{
1455 for (QPhysicsWorld *world : worldManager.worlds) {
1456 if (!world->m_scene) {
1457 continue;
1458 }
1459
1460 QObject *nodeCurr = joint;
1461
1462 while (nodeCurr->parent()) {
1463 nodeCurr = nodeCurr->parent();
1464 if (nodeCurr == world->m_scene) {
1465 return world;
1466 } else if (auto view3d = qobject_cast<QQuick3DViewport *>(nodeCurr);
1467 view3d && view3d->scene() == world->m_scene) {
1468 // HACK? if the parent is a view3d, check against its "implicit" scene
1469 return world;
1470 }
1471 }
1472 }
1473
1474 return nullptr;
1475}
1476
1477void QPhysicsWorld::matchOrphanNodes()
1478{
1479 // FIXME: does this need thread safety?
1480 if (worldManager.orphanNodes.isEmpty())
1481 return;
1482
1483 qsizetype numNodes = worldManager.orphanNodes.length();
1484 qsizetype idx = 0;
1485
1486 while (idx < numNodes) {
1487 auto node = worldManager.orphanNodes[idx];
1488 auto world = getWorld(node);
1489 if (world == this) {
1490 world->m_newPhysicsNodes.push_back(node);
1491 // swap-erase
1492 worldManager.orphanNodes.swapItemsAt(idx, numNodes - 1);
1493 worldManager.orphanNodes.pop_back();
1494 numNodes--;
1495 } else {
1496 idx++;
1497 }
1498 }
1499}
1500
1501void QPhysicsWorld::matchOrphanJoints()
1502{
1503 if (worldManager.orphanJoints.isEmpty())
1504 return;
1505
1506 qsizetype numNodes = worldManager.orphanJoints.length();
1507 qsizetype idx = 0;
1508
1509 while (idx < numNodes) {
1510 auto node = worldManager.orphanJoints[idx];
1511 auto world = getWorld(node);
1512 if (world == this) {
1513 world->m_joints.push_back(node);
1514 // swap-erase
1515 worldManager.orphanJoints.swapItemsAt(idx, numNodes - 1);
1516 worldManager.orphanJoints.pop_back();
1517 numNodes--;
1518 } else {
1519 idx++;
1520 }
1521 }
1522}
1523
1524void QPhysicsWorld::findPhysicsNodes()
1525{
1526 // This method finds the physics nodes inside the scene pointed to by the
1527 // scene property. This method is necessary to run whenever the scene
1528 // property is changed.
1529 if (m_scene == nullptr)
1530 return;
1531
1532 // Recursively go through all children and add all QAbstractPhysicsNode's
1533 QList<QQuick3DObject *> children = m_scene->childItems();
1534 while (!children.empty()) {
1535 auto child = children.takeFirst();
1536 if (auto converted = qobject_cast<QAbstractPhysicsNode *>(child); converted != nullptr) {
1537 // This should never happen but check anyway.
1538 if (converted->m_backendObject != nullptr) {
1539 qWarning() << "Warning: physics node already associated with a backend node.";
1540 continue;
1541 }
1542
1543 m_newPhysicsNodes.push_back(converted);
1544 worldManager.orphanNodes.removeAll(converted); // No longer orphan
1545 }
1546 children.append(child->childItems());
1547 }
1548}
1549
1550void QPhysicsWorld::emitContactCallbacks()
1551{
1552 for (const QPhysicsWorld::BodyContact &contact : std::as_const(m_registeredContacts)) {
1553 if (m_removedPhysicsNodes.contains(contact.sender)
1554 || m_removedPhysicsNodes.contains(contact.receiver))
1555 continue;
1556 contact.receiver->registerContact(contact.sender, contact.positions, contact.impulses,
1557 contact.normals);
1558 }
1559
1560 m_registeredContacts.clear();
1561}
1562
1563physx::PxPhysics *QPhysicsWorld::getPhysics()
1564{
1565 return StaticPhysXObjects::getReference().physics;
1566}
1567
1568physx::PxCooking *QPhysicsWorld::getCooking()
1569{
1570 return StaticPhysXObjects::getReference().cooking;
1571}
1572
1573physx::PxControllerManager *QPhysicsWorld::controllerManager()
1574{
1575 if (m_physx->scene && !m_physx->controllerManager) {
1576 m_physx->controllerManager = PxCreateControllerManager(*m_physx->scene);
1577 qCDebug(lcQuick3dPhysics) << "Created controller manager" << m_physx->controllerManager;
1578 }
1579 return m_physx->controllerManager;
1580}
1581
1582QQuick3DNode *QPhysicsWorld::scene() const
1583{
1584 return m_scene;
1585}
1586
1587void QPhysicsWorld::setScene(QQuick3DNode *newScene)
1588{
1589 if (m_scene == newScene)
1590 return;
1591
1592 m_scene = newScene;
1593
1594 // Delete all nodes since they are associated with the previous scene
1595 for (auto body : std::as_const(m_physXBodies)) {
1596 deregisterNode(body->frontendNode);
1597 }
1598
1599 // Check if scene is already used by another world
1600 bool sceneOK = true;
1601 for (QPhysicsWorld *world : std::as_const(worldManager.worlds)) {
1602 if (world != this && world->scene() == newScene) {
1603 sceneOK = false;
1604 qWarning() << "Warning: scene already associated with physics world";
1605 }
1606 }
1607
1608 if (sceneOK)
1609 findPhysicsNodes();
1610 emit sceneChanged();
1611}
1612
1613int QPhysicsWorld::numThreads() const
1614{
1615 return m_numThreads;
1616}
1617
1618void QPhysicsWorld::setNumThreads(int newNumThreads)
1619{
1620 newNumThreads = qMin(newNumThreads, 256);
1621
1622 if (m_numThreads == newNumThreads)
1623 return;
1624
1625 m_numThreads = newNumThreads;
1626 emit numThreadsChanged();
1627}
1628
1629bool QPhysicsWorld::reportKinematicKinematicCollisions() const
1630{
1631 return m_reportKinematicKinematicCollisions;
1632}
1633
1634void QPhysicsWorld::setReportKinematicKinematicCollisions(
1635 bool newReportKinematicKinematicCollisions)
1636{
1637 if (m_reportKinematicKinematicCollisions == newReportKinematicKinematicCollisions)
1638 return;
1639 m_reportKinematicKinematicCollisions = newReportKinematicKinematicCollisions;
1640 emit reportKinematicKinematicCollisionsChanged();
1641}
1642
1643bool QPhysicsWorld::reportStaticKinematicCollisions() const
1644{
1645 return m_reportStaticKinematicCollisions;
1646}
1647
1648void QPhysicsWorld::setReportStaticKinematicCollisions(bool newReportStaticKinematicCollisions)
1649{
1650 if (m_reportStaticKinematicCollisions == newReportStaticKinematicCollisions)
1651 return;
1652 m_reportStaticKinematicCollisions = newReportStaticKinematicCollisions;
1653 emit reportStaticKinematicCollisionsChanged();
1654}
1655
1656QPhysicsWorld::QueryStructure QPhysicsWorld::staticQueryStructure() const
1657{
1658 return m_staticQueryStructure;
1659}
1660
1661void QPhysicsWorld::setStaticQueryStructure(QueryStructure newStaticQueryStructure)
1662{
1663 if (m_staticQueryStructure == newStaticQueryStructure)
1664 return;
1665
1666 if (newStaticQueryStructure == QueryStructure::NoStructure) {
1667 qWarning()
1668 << "Warning: The QueryStructure::NoStructure is not supported for staticQueryStructure. Available options: [StaticTree, DynamicTree]";
1669 return;
1670 }
1671
1672 if (m_physicsInitialized) {
1673 qWarning()
1674 << "Warning: Changing 'staticQueryStructure' after physics is initialized will have no effect";
1675 return;
1676 }
1677
1678 m_staticQueryStructure = newStaticQueryStructure;
1679 emit staticQueryStructureChanged();
1680}
1681
1682QPhysicsWorld::QueryStructure QPhysicsWorld::dynamicQueryStructure() const
1683{
1684 return m_dynamicQueryStructure;
1685}
1686
1687void QPhysicsWorld::setDynamicQueryStructure(QueryStructure newDynamicQueryStructure)
1688{
1689 if (m_dynamicQueryStructure == newDynamicQueryStructure)
1690 return;
1691
1692 if (m_physicsInitialized) {
1693 qWarning()
1694 << "Warning: Changing 'dynamicQueryStructure' after physics is initialized will have no effect";
1695 return;
1696 }
1697
1698 m_dynamicQueryStructure = newDynamicQueryStructure;
1699 emit dynamicQueryStructureChanged();
1700}
1701
1703
1704#include "qphysicsworld.moc"
Definition qlist.h:82
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
static QWorldManager worldManager
static void collectPhysicsNodes(QQuick3DObject *node, QList< QAbstractPhysicsNode * > &nodes)
static const QString qtPhysicsTimingsFile
#define QT_BEGIN_NAMESPACE
#define QT_END_NAMESPACE
#define Q_OBJECT
#define emit
QVector< QAbstractPhysicsNode * > orphanNodes
QVector< QPhysicsWorld * > worlds
QVector< QPhysicsJoint * > orphanJoints