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
34QT_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
72/*!
73 \qmlproperty float PhysicsWorld::typicalLength
74 This property defines the approximate size of objects in the simulation. This is used to
75 estimate certain length-related tolerances. Objects much smaller or much larger than this
76 size may not behave properly. The default value is \c 100.
77
78 Range: \c{[0, inf]}
79*/
80
81/*!
82 \qmlproperty float PhysicsWorld::typicalSpeed
83 This property defines the typical magnitude of velocities of objects in simulation. This is used
84 to estimate whether a contact should be treated as bouncing or resting based on its impact
85 velocity, and a kinetic energy threshold below which the simulation may put objects to sleep.
86
87 For normal physical environments, a good choice is the approximate speed of an object falling
88 under gravity for one second. The default value is \c 1000.
89
90 Range: \c{[0, inf]}
91*/
92
93/*!
94 \qmlproperty float PhysicsWorld::defaultDensity
95 This property defines the default density of dynamic objects, measured in kilograms per cubic
96 unit. This is equal to the weight of a cube with side \c 1.
97
98 The default value is \c 0.001, corresponding to 1 g/cm³: the density of water. If your unit of
99 measurement is meters, a good value would be \c 1000. Note that only positive values are
100 allowed.
101
102 Range: \c{(0, inf]}
103*/
104
105/*!
106 \qmlproperty Node PhysicsWorld::viewport
107 This property defines the viewport where debug components will be drawn if \l{forceDebugDraw}
108 is enabled. If unset the \l{scene} node will be used.
109
110 \sa forceDebugDraw, scene
111*/
112
113/*!
114 \qmlproperty float PhysicsWorld::minimumTimestep
115 This property defines the minimum simulation timestep in milliseconds. The default value is
116 \c 16.667 which corresponds to \c 60 frames per second.
117
118 Range: \c{[0, maximumTimestep]}
119*/
120
121/*!
122 \qmlproperty float PhysicsWorld::maximumTimestep
123 This property defines the maximum simulation timestep in milliseconds. The default value is
124 \c 33.333 which corresponds to \c 30 frames per second.
125
126 Range: \c{[0, inf]}
127*/
128
129/*!
130 \qmlproperty Node PhysicsWorld::scene
131
132 This property defines the top-most Node that contains all the nodes of the physical
133 simulation. All physics objects that are an ancestor of this node will be seen as part of this
134 PhysicsWorld.
135
136 \note Using the same scene node for several PhysicsWorld is unsupported.
137*/
138
139/*!
140 \qmlsignal PhysicsWorld::frameDone(float timestep)
141 \since 6.5
142
143 This signal is emitted when the physical simulation is done simulating a frame. The \a timestep
144 parameter is how long in milliseconds the timestep was in the simulation.
145*/
146
147Q_LOGGING_CATEGORY(lcQuick3dPhysics, "qt.quick3d.physics");
148
149static const QQuaternion kMinus90YawRotation = QQuaternion::fromEulerAngles(pitch: 0, yaw: -90, roll: 0);
150
151/////////////////////////////////////////////////////////////////////////////
152
153class SimulationWorker : public QObject
154{
155 Q_OBJECT
156public:
157 SimulationWorker(QPhysXWorld *physx) : m_physx(physx) { }
158
159public slots:
160 void simulateFrame(float minTimestep, float maxTimestep)
161 {
162 if (!m_physx->isRunning) {
163 m_timer.start();
164 m_physx->isRunning = true;
165 }
166
167 // Assuming: 0 <= minTimestep <= maxTimestep
168
169 constexpr auto MILLIONTH = 0.000001;
170
171 // If not enough time has elapsed we sleep until it has
172 auto deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
173 while (deltaMS < minTimestep) {
174 auto sleepUSecs = (minTimestep - deltaMS) * 1000.f;
175 QThread::usleep(sleepUSecs);
176 deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
177 }
178 m_timer.restart();
179
180 auto deltaSecs = qMin(a: float(deltaMS), b: maxTimestep) * 0.001f;
181 m_physx->scene->simulate(elapsedTime: deltaSecs);
182 m_physx->scene->fetchResults(block: true);
183
184 emit frameDone(deltaTime: deltaSecs);
185 }
186
187 void simulateFrameDesignStudio(float minTimestep, float maxTimestep)
188 {
189 Q_UNUSED(minTimestep);
190 Q_UNUSED(maxTimestep);
191 auto sleepUSecs = 16 * 1000.f; // 16 ms
192 QThread::usleep(sleepUSecs);
193 emit frameDoneDesignStudio();
194 }
195
196signals:
197 void frameDone(float deltaTime);
198 void frameDoneDesignStudio();
199
200private:
201 QPhysXWorld *m_physx = nullptr;
202 QElapsedTimer m_timer;
203};
204
205/////////////////////////////////////////////////////////////////////////////
206
207struct QWorldManager
208{
209 QVector<QPhysicsWorld *> worlds;
210 QVector<QAbstractPhysicsNode *> orphanNodes;
211};
212
213static QWorldManager worldManager = QWorldManager {};
214
215void QPhysicsWorld::registerNode(QAbstractPhysicsNode *physicsNode)
216{
217 auto world = getWorld(node: physicsNode);
218 if (world) {
219 world->m_newPhysicsNodes.push_back(t: physicsNode);
220 } else {
221 worldManager.orphanNodes.push_back(t: physicsNode);
222 }
223}
224
225void QPhysicsWorld::deregisterNode(QAbstractPhysicsNode *physicsNode)
226{
227 for (auto world : worldManager.worlds) {
228 world->m_newPhysicsNodes.removeAll(t: physicsNode);
229 if (physicsNode->m_backendObject) {
230 physicsNode->m_backendObject->isRemoved = true;
231 physicsNode->m_backendObject = nullptr;
232 }
233 QMutexLocker locker(&world->m_removedPhysicsNodesMutex);
234 world->m_removedPhysicsNodes.insert(value: physicsNode);
235 }
236 worldManager.orphanNodes.removeAll(t: physicsNode);
237}
238
239QPhysicsWorld::QPhysicsWorld(QObject *parent) : QObject(parent)
240{
241 m_inDesignStudio = !qEnvironmentVariableIsEmpty(varName: "QML_PUPPET_MODE");
242 m_physx = new QPhysXWorld;
243 m_physx->createWorld();
244
245 worldManager.worlds.push_back(t: this);
246 matchOrphanNodes();
247}
248
249QPhysicsWorld::~QPhysicsWorld()
250{
251 m_workerThread.quit();
252 m_workerThread.wait();
253 for (auto body : m_physXBodies) {
254 body->cleanup(m_physx);
255 delete body;
256 }
257 m_physx->deleteWorld();
258 delete m_physx;
259 worldManager.worlds.removeAll(t: this);
260}
261
262void QPhysicsWorld::classBegin() {}
263
264void QPhysicsWorld::componentComplete()
265{
266 if ((!m_running && !m_inDesignStudio) || m_physicsInitialized)
267 return;
268 initPhysics();
269 emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep);
270}
271
272QVector3D QPhysicsWorld::gravity() const
273{
274 return m_gravity;
275}
276
277bool QPhysicsWorld::running() const
278{
279 return m_running;
280}
281
282bool QPhysicsWorld::forceDebugDraw() const
283{
284 return m_forceDebugDraw;
285}
286
287bool QPhysicsWorld::enableCCD() const
288{
289 return m_enableCCD;
290}
291
292float QPhysicsWorld::typicalLength() const
293{
294 return m_typicalLength;
295}
296
297float QPhysicsWorld::typicalSpeed() const
298{
299 return m_typicalSpeed;
300}
301
302bool QPhysicsWorld::isNodeRemoved(QAbstractPhysicsNode *object)
303{
304 return m_removedPhysicsNodes.contains(value: object);
305}
306
307void QPhysicsWorld::setGravity(QVector3D gravity)
308{
309 if (m_gravity == gravity)
310 return;
311
312 m_gravity = gravity;
313 if (m_physx->scene) {
314 m_physx->scene->setGravity(QPhysicsUtils::toPhysXType(qvec: m_gravity));
315 }
316 emit gravityChanged(gravity: m_gravity);
317}
318
319void QPhysicsWorld::setRunning(bool running)
320{
321 if (m_running == running)
322 return;
323
324 m_running = running;
325 if (!m_inDesignStudio) {
326 if (m_running && !m_physicsInitialized)
327 initPhysics();
328 if (m_running)
329 emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep);
330 }
331 emit runningChanged(running: m_running);
332}
333
334void QPhysicsWorld::setForceDebugDraw(bool forceDebugDraw)
335{
336 if (m_forceDebugDraw == forceDebugDraw)
337 return;
338
339 m_forceDebugDraw = forceDebugDraw;
340 if (!m_forceDebugDraw)
341 disableDebugDraw();
342 else
343 updateDebugDraw();
344 emit forceDebugDrawChanged(forceDebugDraw: m_forceDebugDraw);
345}
346
347QQuick3DNode *QPhysicsWorld::viewport() const
348{
349 return m_viewport;
350}
351
352void QPhysicsWorld::setHasIndividualDebugDraw()
353{
354 m_hasIndividualDebugDraw = true;
355}
356
357void QPhysicsWorld::setViewport(QQuick3DNode *viewport)
358{
359 if (m_viewport == viewport)
360 return;
361
362 m_viewport = viewport;
363
364 // TODO: test this
365 for (auto material : m_debugMaterials)
366 delete material;
367 m_debugMaterials.clear();
368
369 for (auto &holder : m_collisionShapeDebugModels) {
370 delete holder.model;
371 }
372 m_collisionShapeDebugModels.clear();
373
374 emit viewportChanged(viewport: m_viewport);
375}
376
377void QPhysicsWorld::setMinimumTimestep(float minTimestep)
378{
379 if (qFuzzyCompare(p1: m_minTimestep, p2: minTimestep))
380 return;
381
382 if (minTimestep > m_maxTimestep) {
383 qWarning(msg: "Minimum timestep greater than maximum timestep, value clamped");
384 minTimestep = qMin(a: minTimestep, b: m_maxTimestep);
385 }
386
387 if (minTimestep < 0.f) {
388 qWarning(msg: "Minimum timestep less than zero, value clamped");
389 minTimestep = qMax(a: minTimestep, b: 0.f);
390 }
391
392 if (qFuzzyCompare(p1: m_minTimestep, p2: minTimestep))
393 return;
394
395 m_minTimestep = minTimestep;
396 emit minimumTimestepChanged(minimumTimestep: m_minTimestep);
397}
398
399void QPhysicsWorld::setMaximumTimestep(float maxTimestep)
400{
401 if (qFuzzyCompare(p1: m_maxTimestep, p2: maxTimestep))
402 return;
403
404 if (maxTimestep < 0.f) {
405 qWarning(msg: "Maximum timestep less than zero, value clamped");
406 maxTimestep = qMax(a: maxTimestep, b: 0.f);
407 }
408
409 if (qFuzzyCompare(p1: m_maxTimestep, p2: maxTimestep))
410 return;
411
412 m_maxTimestep = maxTimestep;
413 emit maximumTimestepChanged(maxTimestep);
414}
415
416void QPhysicsWorld::setupDebugMaterials(QQuick3DNode *sceneNode)
417{
418 if (!m_debugMaterials.isEmpty())
419 return;
420
421 const int lineWidth = m_inDesignStudio ? 1 : 3;
422
423 // These colors match the indices of DebugDrawBodyType enum
424 for (auto color : { QColorConstants::Svg::chartreuse, QColorConstants::Svg::cyan,
425 QColorConstants::Svg::lightsalmon, QColorConstants::Svg::red,
426 QColorConstants::Svg::black }) {
427 auto debugMaterial = new QQuick3DDefaultMaterial();
428 debugMaterial->setLineWidth(lineWidth);
429 debugMaterial->setParentItem(sceneNode);
430 debugMaterial->setParent(sceneNode);
431 debugMaterial->setDiffuseColor(color);
432 debugMaterial->setLighting(QQuick3DDefaultMaterial::NoLighting);
433 debugMaterial->setCullMode(QQuick3DMaterial::NoCulling);
434 m_debugMaterials.push_back(t: debugMaterial);
435 }
436}
437
438void QPhysicsWorld::updateDebugDraw()
439{
440 if (!(m_forceDebugDraw || m_hasIndividualDebugDraw)) {
441 // Nothing to draw, trash all previous models (if any) and return
442 if (!m_collisionShapeDebugModels.isEmpty()) {
443 for (const auto& holder : std::as_const(t&: m_collisionShapeDebugModels))
444 delete holder.model;
445 m_collisionShapeDebugModels.clear();
446 }
447 return;
448 }
449
450 // Use scene node if no viewport has been specified
451 auto sceneNode = m_viewport ? m_viewport : m_scene;
452
453 if (sceneNode == nullptr)
454 return;
455
456 setupDebugMaterials(sceneNode);
457 m_hasIndividualDebugDraw = false;
458
459 // Store the collision shapes we have now so we can clear out the removed ones
460 QSet<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>> currentCollisionShapes;
461 currentCollisionShapes.reserve(size: m_collisionShapeDebugModels.size());
462
463 for (QAbstractPhysXNode *node : m_physXBodies) {
464 if (!node->debugGeometryCapability())
465 continue;
466
467 const auto &collisionShapes = node->frontendNode->getCollisionShapesList();
468 const int materialIdx = static_cast<int>(node->getDebugDrawBodyType());
469 const int length = collisionShapes.length();
470 if (node->shapes.length() < length)
471 continue; // CharacterController has shapes, but not PhysX shapes
472 for (int idx = 0; idx < length; idx++) {
473 const auto collisionShape = collisionShapes[idx];
474
475 if (!m_forceDebugDraw && !collisionShape->enableDebugDraw())
476 continue;
477
478 const auto physXShape = node->shapes[idx];
479 DebugModelHolder &holder =
480 m_collisionShapeDebugModels[std::make_pair(x: collisionShape, y&: node)];
481 auto &model = holder.model;
482
483 currentCollisionShapes.insert(value: std::make_pair(x: collisionShape, y&: node));
484
485 m_hasIndividualDebugDraw =
486 m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
487
488 auto localPose = physXShape->getLocalPose();
489
490 // Create/Update debug view infrastructure
491 if (!model) {
492 model = new QQuick3DModel();
493 model->setParentItem(sceneNode);
494 model->setParent(sceneNode);
495 model->setCastsShadows(false);
496 model->setReceivesShadows(false);
497 model->setCastsReflections(false);
498 }
499
500 { // update or set material
501 auto material = m_debugMaterials[materialIdx];
502 QQmlListReference materialsRef(model, "materials");
503 if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
504 materialsRef.clear();
505 materialsRef.append(material);
506 }
507 }
508
509 switch (physXShape->getGeometryType()) {
510 case physx::PxGeometryType::eBOX: {
511 physx::PxBoxGeometry boxGeometry;
512 physXShape->getBoxGeometry(geometry&: boxGeometry);
513 const auto &halfExtentsOld = holder.halfExtents();
514 const auto halfExtents = QPhysicsUtils::toQtType(vec: boxGeometry.halfExtents);
515 if (!qFuzzyCompare(v1: halfExtentsOld, v2: halfExtents)) {
516 auto geom = QDebugDrawHelper::generateBoxGeometry(halfExtents);
517 geom->setParent(model);
518 model->setGeometry(geom);
519 holder.setHalfExtents(halfExtents);
520 }
521
522 }
523 break;
524
525 case physx::PxGeometryType::eSPHERE: {
526 physx::PxSphereGeometry sphereGeometry;
527 physXShape->getSphereGeometry(geometry&: sphereGeometry);
528 const float radius = holder.radius();
529 if (!qFuzzyCompare(p1: sphereGeometry.radius, p2: radius)) {
530 auto geom = QDebugDrawHelper::generateSphereGeometry(radius: sphereGeometry.radius);
531 geom->setParent(model);
532 model->setGeometry(geom);
533 holder.setRadius(sphereGeometry.radius);
534 }
535 }
536 break;
537
538 case physx::PxGeometryType::eCAPSULE: {
539 physx::PxCapsuleGeometry capsuleGeometry;
540 physXShape->getCapsuleGeometry(geometry&: capsuleGeometry);
541 const float radius = holder.radius();
542 const float halfHeight = holder.halfHeight();
543
544 if (!qFuzzyCompare(p1: capsuleGeometry.radius, p2: radius)
545 || !qFuzzyCompare(p1: capsuleGeometry.halfHeight, p2: halfHeight)) {
546 auto geom = QDebugDrawHelper::generateCapsuleGeometry(
547 radius: capsuleGeometry.radius, halfHeight: capsuleGeometry.halfHeight);
548 geom->setParent(model);
549 model->setGeometry(geom);
550 holder.setRadius(capsuleGeometry.radius);
551 holder.setHalfHeight(capsuleGeometry.halfHeight);
552 }
553 }
554 break;
555
556 case physx::PxGeometryType::ePLANE:{
557 physx::PxPlaneGeometry planeGeometry;
558 physXShape->getPlaneGeometry(geometry&: planeGeometry);
559 // Special rotation
560 const QQuaternion rotation =
561 kMinus90YawRotation * QPhysicsUtils::toQtType(quat: localPose.q);
562 localPose = physx::PxTransform(localPose.p, QPhysicsUtils::toPhysXType(qquat: rotation));
563
564 if (model->geometry() == nullptr) {
565 auto geom = QDebugDrawHelper::generatePlaneGeometry();
566 geom->setParent(model);
567 model->setGeometry(geom);
568 }
569 }
570 break;
571
572 case physx::PxGeometryType::eHEIGHTFIELD: {
573 physx::PxHeightFieldGeometry heightFieldGeometry;
574 physXShape->getHeightFieldGeometry(geometry&: heightFieldGeometry);
575 const float heightScale = holder.heightScale();
576 const float rowScale = holder.rowScale();
577 const float columnScale = holder.columnScale();
578
579 if (!qFuzzyCompare(p1: heightFieldGeometry.heightScale, p2: heightScale)
580 || !qFuzzyCompare(p1: heightFieldGeometry.rowScale, p2: rowScale)
581 || !qFuzzyCompare(p1: heightFieldGeometry.columnScale, p2: columnScale)) {
582
583 auto geom = QDebugDrawHelper::generateHeightFieldGeometry(
584 heightField: heightFieldGeometry.heightField, heightScale: heightFieldGeometry.heightScale,
585 rowScale: heightFieldGeometry.rowScale, columnScale: heightFieldGeometry.columnScale);
586 geom->setParent(model);
587 model->setGeometry(geom);
588 holder.setHeightScale(heightFieldGeometry.heightScale);
589 holder.setRowScale(heightFieldGeometry.rowScale);
590 holder.setColumnScale(heightFieldGeometry.columnScale);
591 }
592 }
593 break;
594
595 case physx::PxGeometryType::eCONVEXMESH: {
596 physx::PxConvexMeshGeometry convexMeshGeometry;
597 physXShape->getConvexMeshGeometry(geometry&: convexMeshGeometry);
598 const auto rotation = convexMeshGeometry.scale.rotation * localPose.q;
599 localPose = physx::PxTransform(localPose.p, rotation);
600 model->setScale(QPhysicsUtils::toQtType(vec: convexMeshGeometry.scale.scale));
601
602 if (model->geometry() == nullptr) {
603 auto geom = QDebugDrawHelper::generateConvexMeshGeometry(
604 convexMesh: convexMeshGeometry.convexMesh);
605 geom->setParent(model);
606 model->setGeometry(geom);
607 }
608 }
609 break;
610
611 case physx::PxGeometryType::eTRIANGLEMESH: {
612 physx::PxTriangleMeshGeometry triangleMeshGeometry;
613 physXShape->getTriangleMeshGeometry(geometry&: triangleMeshGeometry);
614 const auto rotation = triangleMeshGeometry.scale.rotation * localPose.q;
615 localPose = physx::PxTransform(localPose.p, rotation);
616 model->setScale(QPhysicsUtils::toQtType(vec: triangleMeshGeometry.scale.scale));
617
618 if (model->geometry() == nullptr) {
619 auto geom = QDebugDrawHelper::generateTriangleMeshGeometry(
620 triangleMesh: triangleMeshGeometry.triangleMesh);
621 geom->setParent(model);
622 model->setGeometry(geom);
623 }
624 }
625 break;
626
627 case physx::PxGeometryType::eINVALID:
628 case physx::PxGeometryType::eGEOMETRY_COUNT:
629 // should not happen
630 Q_UNREACHABLE();
631 }
632
633 model->setVisible(true);
634
635 auto globalPose = node->getGlobalPose();
636 auto finalPose = globalPose.transform(src: localPose);
637
638 model->setRotation(QPhysicsUtils::toQtType(quat: finalPose.q));
639 model->setPosition(QPhysicsUtils::toQtType(vec: finalPose.p));
640 }
641 }
642
643 // Remove old collision shapes
644 m_collisionShapeDebugModels.removeIf(
645 pred: [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>,
646 DebugModelHolder>::iterator it) {
647 if (!currentCollisionShapes.contains(value: it.key())) {
648 auto holder = it.value();
649 if (holder.model)
650 delete holder.model;
651 return true;
652 }
653 return false;
654 });
655}
656
657static void collectPhysicsNodes(QQuick3DObject *node, QList<QAbstractPhysicsNode *> &nodes)
658{
659 if (auto shape = qobject_cast<QAbstractPhysicsNode *>(object: node)) {
660 nodes.push_back(t: shape);
661 return;
662 }
663
664 for (QQuick3DObject *child : node->childItems())
665 collectPhysicsNodes(node: child, nodes);
666}
667
668void QPhysicsWorld::updateDebugDrawDesignStudio()
669{
670 // Use scene node if no viewport has been specified
671 auto sceneNode = m_viewport ? m_viewport : m_scene;
672
673 if (sceneNode == nullptr)
674 return;
675
676 setupDebugMaterials(sceneNode);
677
678 // Store the collision shapes we have now so we can clear out the removed ones
679 QSet<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>> currentCollisionShapes;
680 currentCollisionShapes.reserve(size: m_collisionShapeDebugModels.size());
681
682 QList<QAbstractPhysicsNode *> activePhysicsNodes;
683 activePhysicsNodes.reserve(size: m_collisionShapeDebugModels.size());
684 collectPhysicsNodes(node: m_scene, nodes&: activePhysicsNodes);
685
686 for (QAbstractPhysicsNode *node : activePhysicsNodes) {
687
688 const auto &collisionShapes = node->getCollisionShapesList();
689 const int materialIdx = 0; // Just take first material
690 const int length = collisionShapes.length();
691
692 const bool isCharacterController = qobject_cast<QCharacterController *>(object: node) != nullptr;
693
694 for (int idx = 0; idx < length; idx++) {
695 QAbstractCollisionShape *collisionShape = collisionShapes[idx];
696 DebugModelHolder &holder =
697 m_DesignStudioDebugModels[std::make_pair(x&: collisionShape, y&: node)];
698 auto &model = holder.model;
699
700 currentCollisionShapes.insert(value: std::make_pair(x&: collisionShape, y&: node));
701
702 m_hasIndividualDebugDraw =
703 m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
704
705 // Create/Update debug view infrastructure
706 {
707 // Hack: we have to delete the model every frame so it shows up in QDS
708 // whenever the code is updated, not sure why ¯\_(?)_/¯
709 delete model;
710 model = new QQuick3DModel();
711 model->setParentItem(sceneNode);
712 model->setParent(sceneNode);
713 model->setCastsShadows(false);
714 model->setReceivesShadows(false);
715 model->setCastsReflections(false);
716 }
717
718 const bool hasGeometry = holder.geometry != nullptr;
719 QVector3D scenePosition = collisionShape->scenePosition();
720 QQuaternion sceneRotation = collisionShape->sceneRotation();
721 QQuick3DGeometry *newGeometry = nullptr;
722
723 if (isCharacterController)
724 sceneRotation = sceneRotation * QQuaternion::fromEulerAngles(eulerAngles: QVector3D(0, 0, 90));
725
726 { // update or set material
727 auto material = m_debugMaterials[materialIdx];
728 QQmlListReference materialsRef(model, "materials");
729 if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
730 materialsRef.clear();
731 materialsRef.append(material);
732 }
733 }
734
735 if (auto shape = qobject_cast<QBoxShape *>(object: collisionShape)) {
736 const auto &halfExtentsOld = holder.halfExtents();
737 const auto halfExtents = shape->sceneScale() * shape->extents() * 0.5f;
738 if (!qFuzzyCompare(v1: halfExtentsOld, v2: halfExtents) || !hasGeometry) {
739 newGeometry = QDebugDrawHelper::generateBoxGeometry(halfExtents);
740 holder.setHalfExtents(halfExtents);
741 }
742 } else if (auto shape = qobject_cast<QSphereShape *>(object: collisionShape)) {
743 const float radiusOld = holder.radius();
744 const float radius = shape->sceneScale().x() * shape->diameter() * 0.5f;
745 if (!qFuzzyCompare(p1: radiusOld, p2: radius) || !hasGeometry) {
746 newGeometry = QDebugDrawHelper::generateSphereGeometry(radius);
747 holder.setRadius(radius);
748 }
749 } else if (auto shape = qobject_cast<QCapsuleShape *>(object: collisionShape)) {
750 const float radiusOld = holder.radius();
751 const float halfHeightOld = holder.halfHeight();
752 const float radius = shape->sceneScale().y() * shape->diameter() * 0.5f;
753 const float halfHeight = shape->sceneScale().x() * shape->height() * 0.5f;
754
755 if ((!qFuzzyCompare(p1: radiusOld, p2: radius) || !qFuzzyCompare(p1: halfHeightOld, p2: halfHeight))
756 || !hasGeometry) {
757 newGeometry = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
758 holder.setRadius(radius);
759 holder.setHalfHeight(halfHeight);
760 }
761 } else if (qobject_cast<QPlaneShape *>(object: collisionShape)) {
762 if (!hasGeometry)
763 newGeometry = QDebugDrawHelper::generatePlaneGeometry();
764 } else if (auto shape = qobject_cast<QHeightFieldShape *>(object: collisionShape)) {
765 physx::PxHeightFieldGeometry *heightFieldGeometry =
766 static_cast<physx::PxHeightFieldGeometry *>(shape->getPhysXGeometry());
767 const float heightScale = holder.heightScale();
768 const float rowScale = holder.rowScale();
769 const float columnScale = holder.columnScale();
770 scenePosition += shape->hfOffset();
771 if (!heightFieldGeometry) {
772 qWarning() << "Could not get height field";
773 } else if (!qFuzzyCompare(p1: heightFieldGeometry->heightScale, p2: heightScale)
774 || !qFuzzyCompare(p1: heightFieldGeometry->rowScale, p2: rowScale)
775 || !qFuzzyCompare(p1: heightFieldGeometry->columnScale, p2: columnScale)
776 || !hasGeometry) {
777 newGeometry = QDebugDrawHelper::generateHeightFieldGeometry(
778 heightField: heightFieldGeometry->heightField, heightScale: heightFieldGeometry->heightScale,
779 rowScale: heightFieldGeometry->rowScale, columnScale: heightFieldGeometry->columnScale);
780 holder.setHeightScale(heightFieldGeometry->heightScale);
781 holder.setRowScale(heightFieldGeometry->rowScale);
782 holder.setColumnScale(heightFieldGeometry->columnScale);
783 }
784 } else if (auto shape = qobject_cast<QConvexMeshShape *>(object: collisionShape)) {
785 auto convexMeshGeometry =
786 static_cast<physx::PxConvexMeshGeometry *>(shape->getPhysXGeometry());
787 if (!convexMeshGeometry) {
788 qWarning() << "Could not get convex mesh";
789 } else {
790 model->setScale(QPhysicsUtils::toQtType(vec: convexMeshGeometry->scale.scale));
791
792 if (!hasGeometry) {
793 newGeometry = QDebugDrawHelper::generateConvexMeshGeometry(
794 convexMesh: convexMeshGeometry->convexMesh);
795 }
796 }
797 } else if (auto shape = qobject_cast<QTriangleMeshShape *>(object: collisionShape)) {
798 physx::PxTriangleMeshGeometry *triangleMeshGeometry =
799 static_cast<physx::PxTriangleMeshGeometry *>(shape->getPhysXGeometry());
800 if (!triangleMeshGeometry) {
801 qWarning() << "Could not get triangle mesh";
802 } else {
803 model->setScale(QPhysicsUtils::toQtType(vec: triangleMeshGeometry->scale.scale));
804
805 if (!hasGeometry) {
806 newGeometry = QDebugDrawHelper::generateTriangleMeshGeometry(
807 triangleMesh: triangleMeshGeometry->triangleMesh);
808 }
809 }
810 }
811
812 if (newGeometry) {
813 delete holder.geometry;
814 holder.geometry = newGeometry;
815 }
816
817 model->setGeometry(holder.geometry);
818 model->setVisible(true);
819
820 model->setRotation(sceneRotation);
821 model->setPosition(scenePosition);
822 }
823 }
824
825 // Remove old debug models
826 m_DesignStudioDebugModels.removeIf(
827 pred: [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>,
828 DebugModelHolder>::iterator it) {
829 if (!currentCollisionShapes.contains(value: it.key())) {
830 auto holder = it.value();
831 if (holder.model) {
832 delete holder.geometry;
833 delete holder.model;
834 }
835 return true;
836 }
837 return false;
838 });
839}
840
841void QPhysicsWorld::disableDebugDraw()
842{
843 m_hasIndividualDebugDraw = false;
844
845 for (QAbstractPhysXNode *body : m_physXBodies) {
846 const auto &collisionShapes = body->frontendNode->getCollisionShapesList();
847 const int length = collisionShapes.length();
848 for (int idx = 0; idx < length; idx++) {
849 const auto collisionShape = collisionShapes[idx];
850 if (collisionShape->enableDebugDraw()) {
851 m_hasIndividualDebugDraw = true;
852 return;
853 }
854 }
855 }
856}
857
858void QPhysicsWorld::setEnableCCD(bool enableCCD)
859{
860 if (m_enableCCD == enableCCD)
861 return;
862
863 if (m_physicsInitialized) {
864 qWarning()
865 << "Warning: Changing 'enableCCD' after physics is initialized will have no effect";
866 return;
867 }
868
869 m_enableCCD = enableCCD;
870 emit enableCCDChanged(enableCCD: m_enableCCD);
871}
872
873void QPhysicsWorld::setTypicalLength(float typicalLength)
874{
875 if (qFuzzyCompare(p1: typicalLength, p2: m_typicalLength))
876 return;
877
878 if (typicalLength <= 0.f) {
879 qWarning() << "Warning: 'typicalLength' value less than zero, ignored";
880 return;
881 }
882
883 if (m_physicsInitialized) {
884 qWarning() << "Warning: Changing 'typicalLength' after physics is initialized will have "
885 "no effect";
886 return;
887 }
888
889 m_typicalLength = typicalLength;
890
891 emit typicalLengthChanged(typicalLength);
892}
893
894void QPhysicsWorld::setTypicalSpeed(float typicalSpeed)
895{
896 if (qFuzzyCompare(p1: typicalSpeed, p2: m_typicalSpeed))
897 return;
898
899 if (m_physicsInitialized) {
900 qWarning() << "Warning: Changing 'typicalSpeed' after physics is initialized will have "
901 "no effect";
902 return;
903 }
904
905 m_typicalSpeed = typicalSpeed;
906
907 emit typicalSpeedChanged(typicalSpeed);
908}
909
910float QPhysicsWorld::defaultDensity() const
911{
912 return m_defaultDensity;
913}
914
915float QPhysicsWorld::minimumTimestep() const
916{
917 return m_minTimestep;
918}
919
920float QPhysicsWorld::maximumTimestep() const
921{
922 return m_maxTimestep;
923}
924
925void QPhysicsWorld::setDefaultDensity(float defaultDensity)
926{
927 if (qFuzzyCompare(p1: m_defaultDensity, p2: defaultDensity))
928 return;
929 m_defaultDensity = defaultDensity;
930
931 // Go through all dynamic rigid bodies and update the default density
932 for (QAbstractPhysXNode *body : m_physXBodies)
933 body->updateDefaultDensity(density: m_defaultDensity);
934
935 emit defaultDensityChanged(defaultDensity);
936}
937
938// Remove physics world items that no longer exist
939
940void QPhysicsWorld::cleanupRemovedNodes()
941{
942 m_physXBodies.removeIf(pred: [this](QAbstractPhysXNode *body) {
943 return body->cleanupIfRemoved(physX: m_physx);
944 });
945 // We don't need to lock the mutex here since the simulation
946 // worker is waiting
947 m_removedPhysicsNodes.clear();
948}
949
950void QPhysicsWorld::initPhysics()
951{
952 Q_ASSERT(!m_physicsInitialized);
953
954 m_physx->createScene(typicalLength: m_typicalLength, typicalSpeed: m_typicalSpeed, gravity: m_gravity, enableCCD: m_enableCCD, physicsWorld: this);
955
956 // Setup worker thread
957 SimulationWorker *worker = new SimulationWorker(m_physx);
958 worker->moveToThread(thread: &m_workerThread);
959 connect(sender: &m_workerThread, signal: &QThread::finished, context: worker, slot: &QObject::deleteLater);
960 if (m_inDesignStudio) {
961 connect(sender: this, signal: &QPhysicsWorld::simulateFrame, context: worker,
962 slot: &SimulationWorker::simulateFrameDesignStudio);
963 connect(sender: worker, signal: &SimulationWorker::frameDoneDesignStudio, context: this,
964 slot: &QPhysicsWorld::frameFinishedDesignStudio);
965 } else {
966 connect(sender: this, signal: &QPhysicsWorld::simulateFrame, context: worker, slot: &SimulationWorker::simulateFrame);
967 connect(sender: worker, signal: &SimulationWorker::frameDone, context: this, slot: &QPhysicsWorld::frameFinished);
968 }
969 m_workerThread.start();
970
971 m_physicsInitialized = true;
972}
973
974void QPhysicsWorld::frameFinished(float deltaTime)
975{
976 matchOrphanNodes();
977 cleanupRemovedNodes();
978 for (auto *node : std::as_const(t&: m_newPhysicsNodes)) {
979 auto *body = node->createPhysXBackend();
980 body->init(world: this, physX: m_physx);
981 m_physXBodies.push_back(t: body);
982 }
983 m_newPhysicsNodes.clear();
984
985 QHash<QQuick3DNode *, QMatrix4x4> transformCache;
986
987 // TODO: Use dirty flag/dirty list to avoid redoing things that didn't change
988 for (auto *physXBody : std::as_const(t&: m_physXBodies)) {
989 physXBody->markDirtyShapes();
990 physXBody->rebuildDirtyShapes(this, m_physx);
991
992 // Sync the physics world and the scene
993 physXBody->sync(deltaTime, transformCache);
994 }
995
996 updateDebugDraw();
997
998 if (m_running)
999 emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep);
1000 emit frameDone(timestep: deltaTime * 1000);
1001}
1002
1003void QPhysicsWorld::frameFinishedDesignStudio()
1004{
1005 // Note sure if this is needed but do it anyway
1006 matchOrphanNodes();
1007 cleanupRemovedNodes();
1008 // Ignore new physics nodes, we find them from the scene node anyway
1009 m_newPhysicsNodes.clear();
1010
1011 updateDebugDrawDesignStudio();
1012
1013 emit simulateFrame(minTimestep: m_minTimestep, maxTimestep: m_maxTimestep);
1014}
1015
1016QPhysicsWorld *QPhysicsWorld::getWorld(QQuick3DNode *node)
1017{
1018 for (QPhysicsWorld *world : worldManager.worlds) {
1019 if (!world->m_scene) {
1020 continue;
1021 }
1022
1023 QQuick3DNode *nodeCurr = node;
1024
1025 // Maybe pointless but check starting node
1026 if (nodeCurr == world->m_scene)
1027 return world;
1028
1029 while (nodeCurr->parentNode()) {
1030 nodeCurr = nodeCurr->parentNode();
1031 if (nodeCurr == world->m_scene)
1032 return world;
1033 }
1034 }
1035
1036 return nullptr;
1037}
1038
1039void QPhysicsWorld::matchOrphanNodes()
1040{
1041 // FIXME: does this need thread safety?
1042 if (worldManager.orphanNodes.isEmpty())
1043 return;
1044
1045 qsizetype numNodes = worldManager.orphanNodes.length();
1046 qsizetype idx = 0;
1047
1048 while (idx < numNodes) {
1049 auto node = worldManager.orphanNodes[idx];
1050 auto world = getWorld(node);
1051 if (world == this) {
1052 world->m_newPhysicsNodes.push_back(t: node);
1053 // swap-erase
1054 worldManager.orphanNodes.swapItemsAt(i: idx, j: numNodes - 1);
1055 worldManager.orphanNodes.pop_back();
1056 numNodes--;
1057 } else {
1058 idx++;
1059 }
1060 }
1061}
1062
1063void QPhysicsWorld::findPhysicsNodes()
1064{
1065 // This method finds the physics nodes inside the scene pointed to by the
1066 // scene property. This method is necessary to run whenever the scene
1067 // property is changed.
1068 if (m_scene == nullptr)
1069 return;
1070
1071 // Recursively go through all children and add all QAbstractPhysicsNode's
1072 QList<QQuick3DObject *> children = m_scene->childItems();
1073 while (!children.empty()) {
1074 auto child = children.takeFirst();
1075 if (auto converted = qobject_cast<QAbstractPhysicsNode *>(object: child); converted != nullptr) {
1076 // This should never happen but check anyway.
1077 if (converted->m_backendObject != nullptr) {
1078 qWarning() << "Warning: physics node already associated with a backend node.";
1079 continue;
1080 }
1081
1082 m_newPhysicsNodes.push_back(t: converted);
1083 worldManager.orphanNodes.removeAll(t: converted); // No longer orphan
1084 }
1085 children.append(l: child->childItems());
1086 }
1087}
1088
1089physx::PxPhysics *QPhysicsWorld::getPhysics()
1090{
1091 return StaticPhysXObjects::getReference().physics;
1092}
1093
1094physx::PxCooking *QPhysicsWorld::getCooking()
1095{
1096 return StaticPhysXObjects::getReference().cooking;
1097}
1098
1099physx::PxControllerManager *QPhysicsWorld::controllerManager()
1100{
1101 if (m_physx->scene && !m_physx->controllerManager) {
1102 m_physx->controllerManager = PxCreateControllerManager(scene&: *m_physx->scene);
1103 qCDebug(lcQuick3dPhysics) << "Created controller manager" << m_physx->controllerManager;
1104 }
1105 return m_physx->controllerManager;
1106}
1107
1108QQuick3DNode *QPhysicsWorld::scene() const
1109{
1110 return m_scene;
1111}
1112
1113void QPhysicsWorld::setScene(QQuick3DNode *newScene)
1114{
1115 if (m_scene == newScene)
1116 return;
1117
1118 m_scene = newScene;
1119
1120 // Delete all nodes since they are associated with the previous scene
1121 for (auto body : m_physXBodies) {
1122 deregisterNode(physicsNode: body->frontendNode);
1123 }
1124
1125 // Check if scene is already used by another world
1126 bool sceneOK = true;
1127 for (QPhysicsWorld *world : worldManager.worlds) {
1128 if (world != this && world->scene() == newScene) {
1129 sceneOK = false;
1130 qWarning() << "Warning: scene already associated with physics world";
1131 }
1132 }
1133
1134 if (sceneOK)
1135 findPhysicsNodes();
1136 emit sceneChanged();
1137}
1138
1139QT_END_NAMESPACE
1140
1141#include "qphysicsworld.moc"
1142

source code of qtquick3dphysics/src/quick3dphysics/qphysicsworld.cpp