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#include "qtriggerbody_p.h"
23
24#include "PxPhysicsAPI.h"
25#include "cooking/PxCooking.h"
26#include <foundation/PxSimpleTypes.h>
27
28#include <QtQuick/private/qquickframeanimation_p.h>
29#include <QtQuick3D/private/qquick3dobject_p.h>
30#include <QtQuick3D/private/qquick3dnode_p.h>
31#include <QtQuick3D/private/qquick3dmodel_p.h>
32#include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>
33#include <QtQuick3DUtils/private/qssgutils_p.h>
34
35#include <QtCore/qvarlengtharray.h>
36#include <QtGui/qquaternion.h>
37
38#include <QtEnvironmentVariables>
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
299/*!
300 \qmlmethod bool PhysicsWorld::testRaycastQuery(vector3d origin, vector3d direction,
301 real maxDistance,
302 bool includeStatic = true,
303 bool includeDynamic = true)
304 \since 6.13
305
306 Performs a fast occlusion check along a ray without computing precise hit
307 geometry or returning hit data.
308
309 Returns \c true if the ray hits a body within \a maxDistance; otherwise returns
310 \c false. The query stops at the first body it finds, which makes it the cheapest
311 of the raycast queries.
312
313 \list
314 \li \a origin is the starting position of the ray, in world space.
315 \li \a direction is the direction of the ray. It must not be a null vector and does
316 not need to be normalized; the query normalizes it.
317 \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
318 must be greater than 0.
319 \li \a includeStatic includes static bodies in the query when \c true.
320 \li \a includeDynamic includes dynamic bodies in the query when \c true.
321 \endlist
322
323 \include qtquick3dphysics-queries.qdocinc include_flags
324
325 \include qtquick3dphysics-queries.qdocinc query_exclusions
326
327 \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
328
329 \sa singleRaycastQuery, multiRaycastQuery, {Qt Quick 3D Physics Scene Queries}
330*/
331
332/*!
333 \qmlmethod locationHit PhysicsWorld::singleRaycastQuery(vector3d origin,
334 vector3d direction,
335 real maxDistance,
336 bool includeStatic = true,
337 bool includeDynamic = true)
338 \since 6.13
339
340 Casts a ray through the physics scene and returns the closest body it hits.
341
342 The ray starts at \a origin and extends along \a direction up to \a maxDistance.
343 Returns a \l locationHit describing the closest intersection.
344
345 \list
346 \li \a origin is the starting position of the ray, in world space.
347 \li \a direction is the direction of the ray. It must not be a null vector and does
348 not need to be normalized; the query normalizes it.
349 \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
350 must be greater than 0.
351 \li \a includeStatic includes static bodies in the query when \c true.
352 \li \a includeDynamic includes dynamic bodies in the query when \c true.
353 \endlist
354
355 \include qtquick3dphysics-queries.qdocinc include_flags
356
357 \include qtquick3dphysics-queries.qdocinc hit_miss_return
358
359 \include qtquick3dphysics-queries.qdocinc query_exclusions
360
361 \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
362
363 \sa testRaycastQuery, multiRaycastQuery, {Qt Quick 3D Physics Scene Queries}
364*/
365
366/*!
367 \qmlmethod list<locationHit> PhysicsWorld::multiRaycastQuery(vector3d origin,
368 vector3d direction,
369 real maxDistance,
370 bool includeStatic = true,
371 bool includeDynamic = true)
372 \since 6.13
373
374 Casts a ray through the physics scene and returns every body it passes through,
375 rather than stopping at the closest one.
376
377 The ray starts at \a origin and extends along \a direction up to \a maxDistance.
378 Returns a \l locationHit for each body hit, or an empty list if the ray hits
379 nothing.
380
381 \list
382 \li \a origin is the starting position of the ray, in world space.
383 \li \a direction is the direction of the ray. It must not be a null vector and does
384 not need to be normalized; the query normalizes it.
385 \li \a maxDistance is the maximum distance along \a direction to cast the ray. It
386 must be greater than 0.
387 \li \a includeStatic includes static bodies in the query when \c true.
388 \li \a includeDynamic includes dynamic bodies in the query when \c true.
389 \endlist
390
391 \include qtquick3dphysics-queries.qdocinc include_flags
392
393 \include qtquick3dphysics-queries.qdocinc query_exclusions
394
395 \include qtquick3dphysics-queries.qdocinc mesh_raycast_limits
396
397 \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
398
399 \include qtquick3dphysics-queries.qdocinc unordered_hits_note
400
401 \sa testRaycastQuery, singleRaycastQuery, {Qt Quick 3D Physics Scene Queries}
402*/
403
404/*!
405 \qmlmethod bool PhysicsWorld::testSweepQuery(CollisionShape shape, vector3d direction,
406 real maxDistance,
407 bool includeStatic = true,
408 bool includeDynamic = true)
409 \since 6.13
410
411 Sweeps a collision shape along a straight path and reports whether the path is
412 obstructed, without computing precise hit geometry.
413
414 Returns \c true if \a shape hits a body before travelling \a maxDistance; otherwise
415 returns \c false. The query stops at the first body it finds, which makes it the
416 cheapest of the sweep queries.
417
418 \list
419 \li \a shape is the \l CollisionShape to sweep through the scene.
420 \li \a direction is the direction of the sweep. It must not be a null vector and does
421 not need to be normalized; the query normalizes it.
422 \li \a maxDistance is the maximum distance along \a direction to travel. It must be
423 greater than 0.
424 \li \a includeStatic includes static bodies in the query when \c true.
425 \li \a includeDynamic includes dynamic bodies in the query when \c true.
426 \endlist
427
428 \include qtquick3dphysics-queries.qdocinc query_shape
429
430 \include qtquick3dphysics-queries.qdocinc include_flags
431
432 \include qtquick3dphysics-queries.qdocinc query_exclusions
433
434 \sa singleSweepQuery, multiSweepQuery, {Qt Quick 3D Physics Scene Queries}
435*/
436
437/*!
438 \qmlmethod locationHit PhysicsWorld::singleSweepQuery(CollisionShape shape,
439 vector3d direction,
440 real maxDistance,
441 bool includeStatic = true,
442 bool includeDynamic = true)
443 \since 6.13
444
445 Sweeps a collision shape along a straight path and returns the closest body it
446 hits.
447
448 Returns a \l locationHit describing the closest intersection encountered along the
449 sweep.
450
451 \list
452 \li \a shape is the \l CollisionShape to sweep through the scene.
453 \li \a direction is the direction of the sweep. It must not be a null vector and does
454 not need to be normalized; the query normalizes it.
455 \li \a maxDistance is the maximum distance along \a direction to travel. It must be
456 greater than 0.
457 \li \a includeStatic includes static bodies in the query when \c true.
458 \li \a includeDynamic includes dynamic bodies in the query when \c true.
459 \endlist
460
461 \include qtquick3dphysics-queries.qdocinc query_shape
462
463 \include qtquick3dphysics-queries.qdocinc include_flags
464
465 \include qtquick3dphysics-queries.qdocinc hit_miss_return
466
467 \include qtquick3dphysics-queries.qdocinc query_exclusions
468
469 \include qtquick3dphysics-queries.qdocinc mtd_overlap_note
470
471 \sa testSweepQuery, multiSweepQuery, {Qt Quick 3D Physics Scene Queries}
472*/
473
474/*!
475 \qmlmethod list<locationHit> PhysicsWorld::multiSweepQuery(CollisionShape shape,
476 vector3d direction,
477 real maxDistance,
478 bool includeStatic = true,
479 bool includeDynamic = true)
480 \since 6.13
481
482 Sweeps a collision shape along a straight path and returns every body it passes
483 through, rather than stopping at the closest one.
484
485 Returns a \l locationHit for each body hit, or an empty list if the sweep hits
486 nothing.
487
488 \list
489 \li \a shape is the \l CollisionShape to sweep through the scene.
490 \li \a direction is the direction of the sweep. It must not be a null vector and does
491 not need to be normalized; the query normalizes it.
492 \li \a maxDistance is the maximum distance along \a direction to travel. It must be
493 greater than 0.
494 \li \a includeStatic includes static bodies in the query when \c true.
495 \li \a includeDynamic includes dynamic bodies in the query when \c true.
496 \endlist
497
498 \include qtquick3dphysics-queries.qdocinc query_shape
499
500 \include qtquick3dphysics-queries.qdocinc include_flags
501
502 \include qtquick3dphysics-queries.qdocinc query_exclusions
503
504 \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
505
506 \include qtquick3dphysics-queries.qdocinc unordered_hits_note
507
508 \include qtquick3dphysics-queries.qdocinc mtd_overlap_note
509
510 \sa testSweepQuery, singleSweepQuery, {Qt Quick 3D Physics Scene Queries}
511*/
512
513/*!
514 \qmlmethod bool PhysicsWorld::testOverlapQuery(CollisionShape shape,
515 bool includeStatic = true,
516 bool includeDynamic = true)
517 \since 6.13
518
519 Reports whether any body occupies the volume of a stationary collision shape.
520
521 Returns \c true if \a shape overlaps a body; otherwise returns \c false. The query
522 stops at the first body it finds and builds no hit data, which makes it the cheapest
523 of the overlap queries.
524
525 \list
526 \li \a shape is the \l CollisionShape defining the overlap volume.
527 \li \a includeStatic includes static bodies in the query when \c true.
528 \li \a includeDynamic includes dynamic bodies in the query when \c true.
529 \endlist
530
531 \include qtquick3dphysics-queries.qdocinc query_shape
532
533 \include qtquick3dphysics-queries.qdocinc include_flags
534
535 \include qtquick3dphysics-queries.qdocinc query_exclusions
536
537 \sa multiOverlapQuery, {Qt Quick 3D Physics Scene Queries}
538*/
539
540/*!
541 \qmlmethod list<queryHit> PhysicsWorld::multiOverlapQuery(CollisionShape shape,
542 bool includeStatic = true,
543 bool includeDynamic = true)
544 \since 6.13
545
546 Returns every body that overlaps the volume of a stationary collision shape.
547
548 Returns a \l queryHit for each overlapping body, or an empty list if the volume is
549 unoccupied.
550
551 \list
552 \li \a shape is the \l CollisionShape defining the overlap volume.
553 \li \a includeStatic includes static bodies in the query when \c true.
554 \li \a includeDynamic includes dynamic bodies in the query when \c true.
555 \endlist
556
557 \include qtquick3dphysics-queries.qdocinc query_shape
558
559 \include qtquick3dphysics-queries.qdocinc include_flags
560
561 \include qtquick3dphysics-queries.qdocinc query_exclusions
562
563 \include qtquick3dphysics-queries.qdocinc mesh_multi_intersection
564
565 \include qtquick3dphysics-queries.qdocinc unordered_hits_note
566
567 \sa testOverlapQuery, {Qt Quick 3D Physics Scene Queries}
568*/
569
570Q_LOGGING_CATEGORY(lcQuick3dPhysics, "qt.quick3d.physics");
571
572// Setting QT_PHYSICS_TIMINGS_FILE to a filepath will generate a csv file with frame timings.
573// Note that if running several PhysicsWorld's the last one to be destructed will overwrite
574// the output file.
575//
576// To view it in gnuplot, run these two commands:
577//
578// set datafile separator ','
579// plot '<QT_PHYSICS_TIMINGS_FILE>' using 1:2 with lines
580//
581// Security note: This file is trusted since it is opened as write-only
582static const QString qtPhysicsTimingsFile = qEnvironmentVariable("QT_PHYSICS_TIMINGS_FILE");
583
584/////////////////////////////////////////////////////////////////////////////
585
586static bool validateQuery(const physx::PxScene *scene,
587 bool includeStatic,
588 bool includeDynamic)
589{
590 if (!scene) {
591 qWarning() << "QtQuick3DPhysics: cannot run a scene query before the physics world is "
592 "initialized.";
593 return false;
594 }
595
596 if (!(includeStatic || includeDynamic)) {
597 qWarning() << "QtQuick3DPhysics: a scene query needs includeStatic or includeDynamic to "
598 "be true.";
599 return false;
600 }
601
602 return true;
603}
604
605static bool raycastImpl(const physx::PxScene *scene,
606 const QVector3D &origin,
607 const QVector3D &direction,
608 float maxDistance,
609 bool includeStatic,
610 bool includeDynamic,
611 physx::PxRaycastCallback &hitCallback,
612 physx::PxHitFlags hitFlags,
613 physx::PxQueryFlags extraQueryFlags = {})
614{
615 if (!validateQuery(scene, includeStatic, includeDynamic))
616 return false;
617
618 const physx::PxVec3 pxOrigin = QPhysicsUtils::toPhysXType(origin);
619 if (!pxOrigin.isFinite()) {
620 qWarning() << "QtQuick3DPhysics: the query origin must be finite.";
621 return false;
622 }
623
624 const physx::PxVec3 pxDirection = QPhysicsUtils::toPhysXType(direction.normalized());
625 if (!pxDirection.isNormalized()) {
626 qWarning() << "QtQuick3DPhysics: the query direction must be a non-null, finite vector.";
627 return false;
628 }
629
630 if (!qIsFinite(maxDistance) || maxDistance <= 0) {
631 qWarning() << "QtQuick3DPhysics: the query distance must be finite and greater than 0.";
632 return false;
633 }
634
635 // Combine base flags with optional query flags (e.g., eANY_HIT or eNO_BLOCK)
636 physx::PxQueryFlags filter = extraQueryFlags;
637 if (includeStatic)
638 filter |= physx::PxQueryFlag::eSTATIC;
639 if (includeDynamic)
640 filter |= physx::PxQueryFlag::eDYNAMIC;
641
642 // Execute PhysX scene raycast
643 return scene->raycast(pxOrigin, pxDirection, maxDistance, hitCallback, hitFlags,
644 physx::PxQueryFilterData(filter));
645}
646
647static const physx::PxGeometry *validateQueryShape(const physx::PxScene *scene,
648 QAbstractCollisionShape *shape,
649 bool includeStatic,
650 bool includeDynamic)
651{
652 if (!validateQuery(scene, includeStatic, includeDynamic)) {
653 return nullptr;
654 }
655
656 const physx::PxGeometry *geometry = shape ? shape->getPhysXGeometry() : nullptr;
657 if (!geometry) {
658 qWarning() << "QtQuick3DPhysics: the query shape has no geometry yet.";
659 return nullptr;
660 }
661
662 // PhysX only supports sphere, capsule, box and convex mesh as the query geometry. Anything
663 // else leaves Gu::ShapeData's bounds uninitialized in a release build.
664 switch (geometry->getType()) {
665 case physx::PxGeometryType::eSPHERE:
666 case physx::PxGeometryType::eCAPSULE:
667 case physx::PxGeometryType::eBOX:
668 case physx::PxGeometryType::eCONVEXMESH:
669 break;
670 default:
671 qWarning() << "QtQuick3DPhysics: sweep and overlap queries only support SphereShape, "
672 "CapsuleShape, BoxShape and ConvexMeshShape as the query shape.";
673 return nullptr;
674 }
675
676 return geometry;
677}
678
679static bool sweepImpl(const physx::PxScene *scene,
680 QAbstractCollisionShape *shape,
681 const QVector3D &direction,
682 float maxDistance,
683 bool includeStatic,
684 bool includeDynamic,
685 physx::PxSweepCallback &hitCallback,
686 physx::PxHitFlags hitFlags,
687 physx::PxQueryFlags extraQueryFlags = {})
688{
689 const physx::PxGeometry *geometry = validateQueryShape(scene, shape, includeStatic, includeDynamic);
690 if (!geometry) {
691 return false;
692 }
693
694 const physx::PxVec3 pxDirection = QPhysicsUtils::toPhysXType(direction.normalized());
695 if (!pxDirection.isNormalized()) {
696 qWarning() << "QtQuick3DPhysics: the query direction must be a non-null, finite vector.";
697 return false;
698 }
699
700 if (!qIsFinite(maxDistance) || maxDistance <= 0) {
701 qWarning() << "QtQuick3DPhysics: the query distance must be finite and greater than 0.";
702 return false;
703 }
704
705 // Combine base flags with optional query flags (e.g., eANY_HIT or eNO_BLOCK)
706 physx::PxQueryFlags filter = extraQueryFlags;
707 if (includeStatic)
708 filter |= physx::PxQueryFlag::eSTATIC;
709 if (includeDynamic)
710 filter |= physx::PxQueryFlag::eDYNAMIC;
711
712 const physx::PxTransform pose(QPhysicsUtils::toPhysXType(shape->scenePosition()),
713 QPhysicsUtils::toPhysXType(shape->sceneRotation()));
714 if (!pose.isSane()) {
715 qWarning() << "QtQuick3DPhysics: the query position/rotation is not finite.";
716 return false;
717 }
718
719 // Execute PhysX scene sweep
720 return scene->sweep(*geometry, pose, pxDirection, maxDistance, hitCallback, hitFlags,
721 physx::PxQueryFilterData(filter));
722}
723
724static bool overlapImpl(const physx::PxScene *scene,
725 QAbstractCollisionShape *shape,
726 bool includeStatic,
727 bool includeDynamic,
728 physx::PxOverlapCallback &hitCallback,
729 physx::PxQueryFlags extraQueryFlags = {})
730{
731
732 const physx::PxGeometry *geometry = validateQueryShape(scene, shape, includeStatic, includeDynamic);
733 if (!geometry) {
734 return false;
735 }
736
737 // Combine base static/dynamic flags with query behavior flags (e.g., eANY_HIT or eNO_BLOCK)
738 physx::PxQueryFlags filter = extraQueryFlags;
739 if (includeStatic)
740 filter |= physx::PxQueryFlag::eSTATIC;
741 if (includeDynamic)
742 filter |= physx::PxQueryFlag::eDYNAMIC;
743
744 const physx::PxTransform pose(QPhysicsUtils::toPhysXType(shape->scenePosition()),
745 QPhysicsUtils::toPhysXType(shape->sceneRotation()));
746 if (!pose.isSane()) {
747 qWarning() << "QtQuick3DPhysics: the query position/rotation is not finite.";
748 return false;
749 }
750
751 // Execute PhysX scene overlap query
752 return scene->overlap(*geometry, pose, hitCallback,
753 physx::PxQueryFilterData(filter));
754}
755
757{
759public:
761 {
762 // Needed to start the frame animation
763 classBegin();
765 }
766};
767
768namespace {
769
770// Collects every touching hit from a scene query. PhysX needs a caller-owned buffer to sort
771// into, and hands the hits over in batches of at most BufferSize once it fills up, so the
772// buffer stays and processTouches() appends each batch to hits.
773// A struct because the PhysX callback bases declare all their members public.
774template<class PxCbT, class HitT, class PxHitT, physx::PxU32 BufferSize = 16>
775struct DynamicQueryCallback : public PxCbT
776{
777 DynamicQueryCallback() : PxCbT(localBuffer, BufferSize) {}
778
779 physx::PxAgain processTouches(const PxHitT *buffer, physx::PxU32 nbHits) override
780 {
781 hits.reserve(hits.size() + nbHits);
782 for (physx::PxU32 i = 0; i < nbHits; ++i)
783 hits.emplaceBack(buffer[i], buffer[i]);
784 return true;
785 }
786
787 PxHitT localBuffer[BufferSize];
788 QList<HitT> hits;
789};
790
791} // namespace
792
793/////////////////////////////////////////////////////////////////////////////
794
795void QPhysicsWorld::DebugModelHolder::releaseMeshPointer()
796{
797 if (auto base = static_cast<physx::PxBase *>(ptr); base)
798 base->release();
799 ptr = nullptr;
800}
801
802const QVector3D &QPhysicsWorld::DebugModelHolder::halfExtents() const
803{
804 return data;
805}
806void QPhysicsWorld::DebugModelHolder::setHalfExtents(const QVector3D &halfExtents)
807{
808 data = halfExtents;
809}
810float QPhysicsWorld::DebugModelHolder::radius() const
811{
812 return data.x();
813}
814void QPhysicsWorld::DebugModelHolder::setRadius(float radius)
815{
816 data.setX(radius);
817}
818float QPhysicsWorld::DebugModelHolder::heightScale() const
819{
820 return data.x();
821}
822void QPhysicsWorld::DebugModelHolder::setHeightScale(float heightScale)
823{
824 data.setX(heightScale);
825}
826float QPhysicsWorld::DebugModelHolder::halfHeight() const
827{
828 return data.y();
829}
830void QPhysicsWorld::DebugModelHolder::setHalfHeight(float halfHeight)
831{
832 data.setY(halfHeight);
833}
834float QPhysicsWorld::DebugModelHolder::rowScale() const
835{
836 return data.y();
837}
838void QPhysicsWorld::DebugModelHolder::setRowScale(float rowScale)
839{
840 data.setY(rowScale);
841}
842float QPhysicsWorld::DebugModelHolder::columnScale() const
843{
844 return data.z();
845}
846void QPhysicsWorld::DebugModelHolder::setColumnScale(float columnScale)
847{
848 data.setZ(columnScale);
849}
850physx::PxConvexMesh *QPhysicsWorld::DebugModelHolder::getConvexMesh()
851{
852 return static_cast<physx::PxConvexMesh *>(ptr);
853}
854void QPhysicsWorld::DebugModelHolder::setConvexMesh(physx::PxConvexMesh *mesh)
855{
856 ptr = static_cast<void *>(mesh);
857}
858physx::PxTriangleMesh *QPhysicsWorld::DebugModelHolder::getTriangleMesh()
859{
860 return static_cast<physx::PxTriangleMesh *>(ptr);
861}
862void QPhysicsWorld::DebugModelHolder::setTriangleMesh(physx::PxTriangleMesh *mesh)
863{
864 ptr = static_cast<void *>(mesh);
865}
866physx::PxHeightField *QPhysicsWorld::DebugModelHolder::getHeightField()
867{
868 return static_cast<physx::PxHeightField *>(ptr);
869}
870void QPhysicsWorld::DebugModelHolder::setHeightField(physx::PxHeightField *hf)
871{
872 ptr = static_cast<physx::PxHeightField *>(hf);
873}
874
875/////////////////////////////////////////////////////////////////////////////
876
883
884static QWorldManager worldManager = QWorldManager {};
885
886void QPhysicsWorld::registerNode(QAbstractPhysicsNode *physicsNode)
887{
888 auto world = getWorld(physicsNode);
889 if (world) {
890 world->m_newPhysicsNodes.push_back(physicsNode);
891 } else {
892 worldManager.orphanNodes.push_back(physicsNode);
893 }
894}
895
896void QPhysicsWorld::deregisterNode(QAbstractPhysicsNode *physicsNode)
897{
898 for (auto world : std::as_const(worldManager.worlds)) {
899 world->m_newPhysicsNodes.removeAll(physicsNode);
900 if (physicsNode->m_backendObject) {
901 Q_ASSERT(physicsNode->m_backendObject->frontendNode == physicsNode);
902 physicsNode->m_backendObject->detachFrontend();
903 physicsNode->m_backendObject->frontendNode = nullptr;
904 physicsNode->m_backendObject->isRemoved = true;
905 physicsNode->m_backendObject = nullptr;
906 }
907 world->m_removedPhysicsNodes.insert(physicsNode);
908 }
909 worldManager.orphanNodes.removeAll(physicsNode);
910}
911
912void QPhysicsWorld::registerJoint(QPhysicsJoint *joint)
913{
914 auto world = getWorld(joint);
915 if (world) {
916 world->m_joints.push_back(joint);
917 } else {
918 worldManager.orphanJoints.push_back(joint);
919 }
920}
921
922void QPhysicsWorld::deregisterJoint(QPhysicsJoint *joint)
923{
924 for (auto world : worldManager.worlds) {
925 world->m_removedJoints.insert(joint->getPhysXBackend());
926
927 // Swap erase since order does not matter
928 qsizetype idx = world->m_joints.indexOf(joint);
929 if (idx != -1) {
930 world->m_joints.swapItemsAt(idx, world->m_joints.size() - 1);
931 world->m_joints.pop_back();
932 }
933 }
934 worldManager.orphanJoints.removeAll(joint);
935}
936
937void QPhysicsWorld::registerContact(QAbstractPhysicsNode *sender, QAbstractPhysicsNode *receiver,
938 const QVector<QVector3D> &positions,
939 const QVector<QVector3D> &impulses,
940 const QVector<QVector3D> &normals)
941{
942 // The callbacks come from fetchResults(), on this thread, but a contact is
943 // between two nodes and reporting the first is free to delete the second.
944 // So the contacts are saved and run at the end of the physics frame, when
945 // it is known which of their nodes are still there.
946
947 BodyContact contact;
948 contact.sender = sender;
949 contact.receiver = receiver;
950 contact.positions = positions;
951 contact.impulses = impulses;
952 contact.normals = normals;
953
954 m_registeredContacts.push_back(contact);
955}
956
957QPhysicsWorld::QPhysicsWorld(QObject *parent) : QObject(parent)
958{
959 m_inDesignStudio = !qEnvironmentVariableIsEmpty("QML_PUPPET_MODE");
960 m_physx = new QPhysXWorld;
961 m_physx->createWorld();
962
963 worldManager.worlds.push_back(this);
964 matchOrphanNodes();
965 matchOrphanJoints();
966
967 m_frameAnimator = new FrameAnimator;
968 connect(m_frameAnimator, &QQuickFrameAnimation::triggered, this,
969 &QPhysicsWorld::simulateFrame);
970}
971
972QPhysicsWorld::~QPhysicsWorld()
973{
974 if (m_frameAnimator) {
975 m_frameAnimator->stop();
976 delete m_frameAnimator;
977 }
978
979 if (m_physx->scene && m_physx->isRunning && !m_frameFetched)
980 m_physx->scene->fetchResults(true);
981
982 for (auto body : std::as_const(m_physXBodies)) {
983 body->cleanup(m_physx);
984 delete body;
985 }
986 m_physXBodies.clear();
987 m_physx->deleteWorld();
988 delete m_physx;
989 worldManager.worlds.removeAll(this);
990
991 if (!qtPhysicsTimingsFile.isEmpty()) {
992 if (m_frameTimings.isEmpty()) {
993 qWarning() << "No frame timings saved.";
994 } else if (auto csvFile = QFile(qtPhysicsTimingsFile); csvFile.open(QIODevice::WriteOnly)) {
995 QTextStream out(&csvFile);
996 for (int i = 1; i < m_frameTimings.size(); i++) {
997 out << i << "," << m_frameTimings[i] << '\n';
998 }
999 csvFile.close();
1000 } else {
1001 qWarning() << "Could not open timings file " << qtPhysicsTimingsFile;
1002 }
1003 }
1004}
1005
1006void QPhysicsWorld::classBegin() {}
1007
1008void QPhysicsWorld::componentComplete()
1009{
1010 if ((!m_running && !m_inDesignStudio) || m_physicsInitialized)
1011 return;
1012 initPhysics();
1013}
1014
1015QVector3D QPhysicsWorld::gravity() const
1016{
1017 return m_gravity;
1018}
1019
1020bool QPhysicsWorld::running() const
1021{
1022 return m_running;
1023}
1024
1025bool QPhysicsWorld::forceDebugDraw() const
1026{
1027 return m_forceDebugDraw;
1028}
1029
1030bool QPhysicsWorld::enableCCD() const
1031{
1032 return m_enableCCD;
1033}
1034
1035float QPhysicsWorld::typicalLength() const
1036{
1037 return m_typicalLength;
1038}
1039
1040float QPhysicsWorld::typicalSpeed() const
1041{
1042 return m_typicalSpeed;
1043}
1044
1045bool QPhysicsWorld::isNodeRemoved(QAbstractPhysicsNode *object)
1046{
1047 return m_removedPhysicsNodes.contains(object);
1048}
1049
1050void QPhysicsWorld::setGravity(QVector3D gravity)
1051{
1052 if (m_gravity == gravity)
1053 return;
1054
1055 if (!QPhysicsUtils::isFinite(gravity)) {
1056 qWarning() << "Warning: 'gravity' must be finite, ignored";
1057 return;
1058 }
1059
1060 m_gravity = gravity;
1061 if (m_physx->scene) {
1062 m_physx->scene->setGravity(QPhysicsUtils::toPhysXType(m_gravity));
1063 }
1064 emit gravityChanged(m_gravity);
1065}
1066
1067void QPhysicsWorld::setRunning(bool running)
1068{
1069 if (m_running == running)
1070 return;
1071
1072 m_running = running;
1073 if (!m_inDesignStudio && m_running && !m_physicsInitialized)
1074 initPhysics();
1075
1076 if (running)
1077 m_frameAnimator->start();
1078 else
1079 m_frameAnimator->stop();
1080
1081 emit runningChanged(m_running);
1082}
1083
1084void QPhysicsWorld::setForceDebugDraw(bool forceDebugDraw)
1085{
1086 if (m_forceDebugDraw == forceDebugDraw)
1087 return;
1088
1089 m_forceDebugDraw = forceDebugDraw;
1090 if (!m_forceDebugDraw)
1091 disableDebugDraw();
1092 else
1093 updateDebugDraw();
1094 emit forceDebugDrawChanged(m_forceDebugDraw);
1095}
1096
1097QQuick3DNode *QPhysicsWorld::viewport() const
1098{
1099 return m_viewport;
1100}
1101
1102void QPhysicsWorld::setHasIndividualDebugDraw()
1103{
1104 m_hasIndividualDebugDraw = true;
1105}
1106
1107void QPhysicsWorld::setViewport(QQuick3DNode *viewport)
1108{
1109 if (m_viewport == viewport)
1110 return;
1111
1112 m_viewport = viewport;
1113
1114 // TODO: test this
1115 for (auto material : std::as_const(m_debugMaterials))
1116 delete material;
1117 m_debugMaterials.clear();
1118
1119 for (auto &holder : m_collisionShapeDebugModels) {
1120 holder.releaseMeshPointer();
1121 delete holder.model;
1122 }
1123 m_collisionShapeDebugModels.clear();
1124
1125 emit viewportChanged(m_viewport);
1126}
1127
1128void QPhysicsWorld::setMinimumTimestep(float minTimestep)
1129{
1130 if (qFuzzyCompare(m_minTimestep, minTimestep))
1131 return;
1132
1133 if (!qIsFinite(minTimestep)) {
1134 qWarning("Minimum timestep must be finite, ignoring");
1135 return;
1136 }
1137
1138 if (minTimestep > m_maxTimestep) {
1139 qWarning("Minimum timestep greater than maximum timestep, value clamped");
1140 minTimestep = qMin(minTimestep, m_maxTimestep);
1141 }
1142
1143 if (minTimestep < 0.f) {
1144 qWarning("Minimum timestep less than zero, value clamped");
1145 minTimestep = qMax(minTimestep, 0.f);
1146 }
1147
1148 if (qFuzzyCompare(m_minTimestep, minTimestep))
1149 return;
1150
1151 m_minTimestep = minTimestep;
1152 emit minimumTimestepChanged(m_minTimestep);
1153}
1154
1155void QPhysicsWorld::setMaximumTimestep(float maxTimestep)
1156{
1157 if (qFuzzyCompare(m_maxTimestep, maxTimestep))
1158 return;
1159
1160 if (!qIsFinite(maxTimestep)) {
1161 qWarning("Maximum timestep must be finite, ignoring");
1162 return;
1163 }
1164
1165 if (maxTimestep < 0.f) {
1166 qWarning("Maximum timestep less than zero, value clamped");
1167 maxTimestep = qMax(maxTimestep, 0.f);
1168 }
1169
1170 if (qFuzzyCompare(m_maxTimestep, maxTimestep))
1171 return;
1172
1173 m_maxTimestep = maxTimestep;
1174 emit maximumTimestepChanged(maxTimestep);
1175}
1176
1177void QPhysicsWorld::setupDebugMaterials(QQuick3DNode *sceneNode)
1178{
1179 if (!m_debugMaterials.isEmpty())
1180 return;
1181
1182 const int lineWidth = m_inDesignStudio ? 1 : 3;
1183
1184 // These colors match the indices of DebugDrawBodyType enum
1185 for (auto color : { QColorConstants::Svg::chartreuse, QColorConstants::Svg::cyan,
1186 QColorConstants::Svg::lightsalmon, QColorConstants::Svg::red,
1187 QColorConstants::Svg::blueviolet, QColorConstants::Svg::black }) {
1188 auto debugMaterial = new QQuick3DPrincipledMaterial();
1189 debugMaterial->setLineWidth(lineWidth);
1190 debugMaterial->setParentItem(sceneNode);
1191 debugMaterial->setParent(sceneNode);
1192 debugMaterial->setBaseColor(color);
1193 debugMaterial->setLighting(QQuick3DPrincipledMaterial::NoLighting);
1194 debugMaterial->setCullMode(QQuick3DMaterial::NoCulling);
1195 m_debugMaterials.push_back(debugMaterial);
1196 }
1197}
1198
1199void QPhysicsWorld::updateDebugDraw()
1200{
1201 if (!(m_forceDebugDraw || m_hasIndividualDebugDraw)) {
1202 // Nothing to draw, trash all previous models (if any) and return
1203 for (auto &holder : m_collisionShapeDebugModels) {
1204 holder.releaseMeshPointer();
1205 delete holder.model;
1206 }
1207 m_collisionShapeDebugModels.clear();
1208 return;
1209 }
1210
1211 // Use scene node if no viewport has been specified
1212 auto sceneNode = m_viewport ? m_viewport : m_scene;
1213
1214 if (sceneNode == nullptr)
1215 return;
1216
1217 setupDebugMaterials(sceneNode);
1218 m_hasIndividualDebugDraw = false;
1219
1220 // Store the collision shapes we have now so we can clear out the removed ones
1221 QSet<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>> currentCollisionShapes;
1222 currentCollisionShapes.reserve(m_collisionShapeDebugModels.size());
1223
1224 for (QAbstractPhysXNode *node : std::as_const(m_physXBodies)) {
1225 // A node deleted from a report earlier in this frame keeps its backend
1226 // until the next one takes it out.
1227 if (!node->frontendNode || !node->debugGeometryCapability())
1228 continue;
1229
1230 const auto &collisionShapes = node->frontendNode->getCollisionShapesList();
1231 const int materialIdx = static_cast<int>(node->getDebugDrawBodyType());
1232 const int length = collisionShapes.length();
1233 for (int idx = 0; idx < length; idx++) {
1234 const auto collisionShape = collisionShapes[idx];
1235
1236 if (!m_forceDebugDraw && !collisionShape->enableDebugDraw())
1237 continue;
1238
1239 DebugModelHolder &holder =
1240 m_collisionShapeDebugModels[std::make_pair(collisionShape, node)];
1241 auto &model = holder.model;
1242
1243 currentCollisionShapes.insert(std::make_pair(collisionShape, node));
1244
1245 m_hasIndividualDebugDraw =
1246 m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
1247
1248 // Create/Update debug view infrastructure
1249 if (!model) {
1250 model = new QQuick3DModel();
1251 model->setParentItem(sceneNode);
1252 model->setParent(sceneNode);
1253 model->setCastsShadows(false);
1254 model->setReceivesShadows(false);
1255 model->setCastsReflections(false);
1256 }
1257
1258 model->setVisible(true);
1259
1260 { // update or set material
1261 auto material = m_debugMaterials[materialIdx];
1262 QQmlListReference materialsRef(model, "materials");
1263 if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
1264 materialsRef.clear();
1265 materialsRef.append(material);
1266 }
1267 }
1268
1269 // Special handling of CharacterController since it has collision shapes,
1270 // but not PhysX shapes
1271 if (qobject_cast<QCharacterController *>(node->frontendNode)) {
1272 QCapsuleShape *capsuleShape = qobject_cast<QCapsuleShape *>(collisionShape);
1273 if (!capsuleShape)
1274 continue;
1275
1276 const float radius = capsuleShape->diameter() * 0.5;
1277 const float halfHeight = capsuleShape->height() * 0.5;
1278
1279 if (!qFuzzyCompare(radius, holder.radius())
1280 || !qFuzzyCompare(halfHeight, holder.halfHeight())) {
1281 auto geom = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
1282 geom->setParent(model);
1283 model->setGeometry(geom);
1284 holder.setRadius(radius);
1285 holder.setHalfHeight(halfHeight);
1286 }
1287
1288 model->setPosition(node->frontendNode->scenePosition());
1289 model->setRotation(node->frontendNode->sceneRotation()
1290 * QQuaternion::fromEulerAngles(0, 0, 90));
1291 continue;
1292 }
1293
1294 if (node->shapes.length() < length)
1295 continue;
1296
1297 const auto physXShape = node->shapes[idx];
1298 auto localPose = physXShape->getLocalPose();
1299
1300 const physx::PxGeometry &geometry = physXShape->getGeometry();
1301
1302 switch (geometry.getType()) {
1303 case physx::PxGeometryType::eBOX: {
1304 const auto &boxGeometry = static_cast<const physx::PxBoxGeometry &>(geometry);
1305 const auto &halfExtentsOld = holder.halfExtents();
1306 const auto halfExtents = QPhysicsUtils::toQtType(boxGeometry.halfExtents);
1307 if (!qFuzzyCompare(halfExtentsOld, halfExtents)) {
1308 auto geom = QDebugDrawHelper::generateBoxGeometry(halfExtents);
1309 geom->setParent(model);
1310 model->setGeometry(geom);
1311 holder.setHalfExtents(halfExtents);
1312 }
1313
1314 }
1315 break;
1316
1317 case physx::PxGeometryType::eSPHERE: {
1318 const auto &sphereGeometry = static_cast<const physx::PxSphereGeometry &>(geometry);
1319 const float radius = holder.radius();
1320 if (!qFuzzyCompare(sphereGeometry.radius, radius)) {
1321 auto geom = QDebugDrawHelper::generateSphereGeometry(sphereGeometry.radius);
1322 geom->setParent(model);
1323 model->setGeometry(geom);
1324 holder.setRadius(sphereGeometry.radius);
1325 }
1326 }
1327 break;
1328
1329 case physx::PxGeometryType::eCAPSULE: {
1330 const auto &capsuleGeometry =
1331 static_cast<const physx::PxCapsuleGeometry &>(geometry);
1332 const float radius = holder.radius();
1333 const float halfHeight = holder.halfHeight();
1334
1335 if (!qFuzzyCompare(capsuleGeometry.radius, radius)
1336 || !qFuzzyCompare(capsuleGeometry.halfHeight, halfHeight)) {
1337 auto geom = QDebugDrawHelper::generateCapsuleGeometry(
1338 capsuleGeometry.radius, capsuleGeometry.halfHeight);
1339 geom->setParent(model);
1340 model->setGeometry(geom);
1341 holder.setRadius(capsuleGeometry.radius);
1342 holder.setHalfHeight(capsuleGeometry.halfHeight);
1343 }
1344 }
1345 break;
1346
1347 case physx::PxGeometryType::ePLANE:{
1348 // Special rotation
1349 const QQuaternion rotation =
1350 QPhysicsUtils::kMinus90YawRotation * QPhysicsUtils::toQtType(localPose.q);
1351 localPose = physx::PxTransform(localPose.p, QPhysicsUtils::toPhysXType(rotation));
1352
1353 if (model->geometry() == nullptr) {
1354 auto geom = QDebugDrawHelper::generatePlaneGeometry();
1355 geom->setParent(model);
1356 model->setGeometry(geom);
1357 }
1358 }
1359 break;
1360
1361 // For heightfield, convex mesh and triangle mesh we increase its reference count
1362 // to make sure it does not get dereferenced and deleted so that the new mesh will
1363 // have another memory address so we know when it has changed.
1364 case physx::PxGeometryType::eHEIGHTFIELD: {
1365 const auto &heightFieldGeometry =
1366 static_cast<const physx::PxHeightFieldGeometry &>(geometry);
1367 const float heightScale = holder.heightScale();
1368 const float rowScale = holder.rowScale();
1369 const float columnScale = holder.columnScale();
1370
1371 if (auto heightField = holder.getHeightField();
1372 heightField && heightField != heightFieldGeometry.heightField) {
1373 heightField->release();
1374 holder.setHeightField(nullptr);
1375 }
1376
1377 if (!qFuzzyCompare(heightFieldGeometry.heightScale, heightScale)
1378 || !qFuzzyCompare(heightFieldGeometry.rowScale, rowScale)
1379 || !qFuzzyCompare(heightFieldGeometry.columnScale, columnScale)
1380 || !holder.getHeightField()) {
1381 if (!holder.getHeightField()) {
1382 heightFieldGeometry.heightField->acquireReference();
1383 holder.setHeightField(heightFieldGeometry.heightField);
1384 }
1385 auto geom = QDebugDrawHelper::generateHeightFieldGeometry(
1386 heightFieldGeometry.heightField, heightFieldGeometry.heightScale,
1387 heightFieldGeometry.rowScale, heightFieldGeometry.columnScale);
1388 geom->setParent(model);
1389 model->setGeometry(geom);
1390 holder.setHeightScale(heightFieldGeometry.heightScale);
1391 holder.setRowScale(heightFieldGeometry.rowScale);
1392 holder.setColumnScale(heightFieldGeometry.columnScale);
1393 }
1394 }
1395 break;
1396
1397 case physx::PxGeometryType::eCONVEXMESH: {
1398 const auto &convexMeshGeometry =
1399 static_cast<const physx::PxConvexMeshGeometry &>(geometry);
1400 const auto rotation = convexMeshGeometry.scale.rotation * localPose.q;
1401 localPose = physx::PxTransform(localPose.p, rotation);
1402 model->setScale(QPhysicsUtils::toQtType(convexMeshGeometry.scale.scale));
1403
1404 if (auto convexMesh = holder.getConvexMesh();
1405 convexMesh && convexMesh != convexMeshGeometry.convexMesh) {
1406 convexMesh->release();
1407 holder.setConvexMesh(nullptr);
1408 }
1409
1410 if (!model->geometry() || !holder.getConvexMesh()) {
1411 if (!holder.getConvexMesh()) {
1412 convexMeshGeometry.convexMesh->acquireReference();
1413 holder.setConvexMesh(convexMeshGeometry.convexMesh);
1414 }
1415 auto geom = QDebugDrawHelper::generateConvexMeshGeometry(
1416 convexMeshGeometry.convexMesh);
1417 geom->setParent(model);
1418 model->setGeometry(geom);
1419 }
1420 }
1421 break;
1422
1423 case physx::PxGeometryType::eTRIANGLEMESH: {
1424 const auto &triangleMeshGeometry =
1425 static_cast<const physx::PxTriangleMeshGeometry &>(geometry);
1426 const auto rotation = triangleMeshGeometry.scale.rotation * localPose.q;
1427 localPose = physx::PxTransform(localPose.p, rotation);
1428 model->setScale(QPhysicsUtils::toQtType(triangleMeshGeometry.scale.scale));
1429
1430 if (auto triangleMesh = holder.getTriangleMesh();
1431 triangleMesh && triangleMesh != triangleMeshGeometry.triangleMesh) {
1432 triangleMesh->release();
1433 holder.setTriangleMesh(nullptr);
1434 }
1435
1436 if (!model->geometry() || !holder.getTriangleMesh()) {
1437 if (!holder.getTriangleMesh()) {
1438 triangleMeshGeometry.triangleMesh->acquireReference();
1439 holder.setTriangleMesh(triangleMeshGeometry.triangleMesh);
1440 }
1441 auto geom = QDebugDrawHelper::generateTriangleMeshGeometry(
1442 triangleMeshGeometry.triangleMesh);
1443 geom->setParent(model);
1444 model->setGeometry(geom);
1445 }
1446 }
1447 break;
1448
1449 // Geometry types that no shape in Qt Quick 3D Physics can have
1450 case physx::PxGeometryType::eCONVEXCORE:
1451 case physx::PxGeometryType::ePARTICLESYSTEM:
1452 case physx::PxGeometryType::eTETRAHEDRONMESH:
1453 case physx::PxGeometryType::eCUSTOM:
1454 case physx::PxGeometryType::eINVALID:
1455 case physx::PxGeometryType::eGEOMETRY_COUNT:
1456 // should not happen
1457 Q_UNREACHABLE();
1458 }
1459
1460 auto globalPose = node->getGlobalPose();
1461 auto finalPose = globalPose.transform(localPose);
1462
1463 model->setRotation(QPhysicsUtils::toQtType(finalPose.q));
1464 model->setPosition(QPhysicsUtils::toQtType(finalPose.p));
1465 }
1466 }
1467
1468 // Remove old collision shapes
1469 m_collisionShapeDebugModels.removeIf(
1470 [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysXNode *>,
1471 DebugModelHolder>::iterator it) {
1472 if (!currentCollisionShapes.contains(it.key())) {
1473 auto holder = it.value();
1474 holder.releaseMeshPointer();
1475 if (holder.model)
1476 delete holder.model;
1477 return true;
1478 }
1479 return false;
1480 });
1481}
1482
1483static void collectPhysicsNodes(QQuick3DObject *node, QList<QAbstractPhysicsNode *> &nodes)
1484{
1485 if (auto shape = qobject_cast<QAbstractPhysicsNode *>(node)) {
1486 nodes.push_back(shape);
1487 return;
1488 }
1489
1490 auto childItems = node->childItems();
1491 for (QQuick3DObject *child : std::as_const(childItems))
1492 collectPhysicsNodes(child, nodes);
1493}
1494
1495void QPhysicsWorld::updateDebugDrawDesignStudio()
1496{
1497 // Use scene node if no viewport has been specified
1498 auto sceneNode = m_viewport ? m_viewport : m_scene;
1499
1500 if (sceneNode == nullptr)
1501 return;
1502
1503 setupDebugMaterials(sceneNode);
1504
1505 // Store the collision shapes we have now so we can clear out the removed ones
1506 QSet<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>> currentCollisionShapes;
1507 currentCollisionShapes.reserve(m_collisionShapeDebugModels.size());
1508
1509 QList<QAbstractPhysicsNode *> activePhysicsNodes;
1510 activePhysicsNodes.reserve(m_collisionShapeDebugModels.size());
1511 collectPhysicsNodes(m_scene, activePhysicsNodes);
1512
1513 for (QAbstractPhysicsNode *node : std::as_const(activePhysicsNodes)) {
1514
1515 const auto &collisionShapes = node->getCollisionShapesList();
1516 const int materialIdx = 0; // Just take first material
1517 const int length = collisionShapes.length();
1518
1519 const bool isCharacterController = qobject_cast<QCharacterController *>(node) != nullptr;
1520
1521 for (int idx = 0; idx < length; idx++) {
1522 QAbstractCollisionShape *collisionShape = collisionShapes[idx];
1523 DebugModelHolder &holder =
1524 m_DesignStudioDebugModels[std::make_pair(collisionShape, node)];
1525 auto &model = holder.model;
1526
1527 currentCollisionShapes.insert(std::make_pair(collisionShape, node));
1528
1529 m_hasIndividualDebugDraw =
1530 m_hasIndividualDebugDraw || collisionShape->enableDebugDraw();
1531
1532 // Create/Update debug view infrastructure
1533 {
1534 // Hack: we have to delete the model every frame so it shows up in QDS
1535 // whenever the code is updated, not sure why ¯\_(?)_/¯
1536 delete model;
1537 model = new QQuick3DModel();
1538 model->setParentItem(sceneNode);
1539 model->setParent(sceneNode);
1540 model->setCastsShadows(false);
1541 model->setReceivesShadows(false);
1542 model->setCastsReflections(false);
1543 }
1544
1545 const bool hasGeometry = holder.geometry != nullptr;
1546 QVector3D scenePosition = collisionShape->scenePosition();
1547 QQuaternion sceneRotation = collisionShape->sceneRotation();
1548 QQuick3DGeometry *newGeometry = nullptr;
1549
1550 if (isCharacterController)
1551 sceneRotation = sceneRotation * QQuaternion::fromEulerAngles(QVector3D(0, 0, 90));
1552
1553 { // update or set material
1554 auto material = m_debugMaterials[materialIdx];
1555 QQmlListReference materialsRef(model, "materials");
1556 if (materialsRef.count() == 0 || materialsRef.at(0) != material) {
1557 materialsRef.clear();
1558 materialsRef.append(material);
1559 }
1560 }
1561
1562 if (auto shape = qobject_cast<QBoxShape *>(collisionShape)) {
1563 const auto &halfExtentsOld = holder.halfExtents();
1564 const auto halfExtents = shape->sceneScale() * shape->extents() * 0.5f;
1565 if (!qFuzzyCompare(halfExtentsOld, halfExtents) || !hasGeometry) {
1566 newGeometry = QDebugDrawHelper::generateBoxGeometry(halfExtents);
1567 holder.setHalfExtents(halfExtents);
1568 }
1569 } else if (auto shape = qobject_cast<QSphereShape *>(collisionShape)) {
1570 const float radiusOld = holder.radius();
1571 const float radius = shape->sceneScale().x() * shape->diameter() * 0.5f;
1572 if (!qFuzzyCompare(radiusOld, radius) || !hasGeometry) {
1573 newGeometry = QDebugDrawHelper::generateSphereGeometry(radius);
1574 holder.setRadius(radius);
1575 }
1576 } else if (auto shape = qobject_cast<QCapsuleShape *>(collisionShape)) {
1577 const float radiusOld = holder.radius();
1578 const float halfHeightOld = holder.halfHeight();
1579 const float radius = shape->sceneScale().y() * shape->diameter() * 0.5f;
1580 const float halfHeight = shape->sceneScale().x() * shape->height() * 0.5f;
1581
1582 if ((!qFuzzyCompare(radiusOld, radius) || !qFuzzyCompare(halfHeightOld, halfHeight))
1583 || !hasGeometry) {
1584 newGeometry = QDebugDrawHelper::generateCapsuleGeometry(radius, halfHeight);
1585 holder.setRadius(radius);
1586 holder.setHalfHeight(halfHeight);
1587 }
1588 } else if (qobject_cast<QPlaneShape *>(collisionShape)) {
1589 if (!hasGeometry)
1590 newGeometry = QDebugDrawHelper::generatePlaneGeometry();
1591 } else if (auto shape = qobject_cast<QHeightFieldShape *>(collisionShape)) {
1592 physx::PxHeightFieldGeometry *heightFieldGeometry =
1593 static_cast<physx::PxHeightFieldGeometry *>(shape->getPhysXGeometry());
1594 const float heightScale = holder.heightScale();
1595 const float rowScale = holder.rowScale();
1596 const float columnScale = holder.columnScale();
1597 scenePosition += shape->hfOffset();
1598 if (!heightFieldGeometry) {
1599 qWarning() << "Could not get height field";
1600 } else if (!qFuzzyCompare(heightFieldGeometry->heightScale, heightScale)
1601 || !qFuzzyCompare(heightFieldGeometry->rowScale, rowScale)
1602 || !qFuzzyCompare(heightFieldGeometry->columnScale, columnScale)
1603 || !hasGeometry) {
1604 newGeometry = QDebugDrawHelper::generateHeightFieldGeometry(
1605 heightFieldGeometry->heightField, heightFieldGeometry->heightScale,
1606 heightFieldGeometry->rowScale, heightFieldGeometry->columnScale);
1607 holder.setHeightScale(heightFieldGeometry->heightScale);
1608 holder.setRowScale(heightFieldGeometry->rowScale);
1609 holder.setColumnScale(heightFieldGeometry->columnScale);
1610 }
1611 } else if (auto shape = qobject_cast<QConvexMeshShape *>(collisionShape)) {
1612 auto convexMeshGeometry =
1613 static_cast<physx::PxConvexMeshGeometry *>(shape->getPhysXGeometry());
1614 if (!convexMeshGeometry) {
1615 qWarning() << "Could not get convex mesh";
1616 } else {
1617 model->setScale(QPhysicsUtils::toQtType(convexMeshGeometry->scale.scale));
1618
1619 if (!hasGeometry) {
1620 newGeometry = QDebugDrawHelper::generateConvexMeshGeometry(
1621 convexMeshGeometry->convexMesh);
1622 }
1623 }
1624 } else if (auto shape = qobject_cast<QTriangleMeshShape *>(collisionShape)) {
1625 physx::PxTriangleMeshGeometry *triangleMeshGeometry =
1626 static_cast<physx::PxTriangleMeshGeometry *>(shape->getPhysXGeometry());
1627 if (!triangleMeshGeometry) {
1628 qWarning() << "Could not get triangle mesh";
1629 } else {
1630 model->setScale(QPhysicsUtils::toQtType(triangleMeshGeometry->scale.scale));
1631
1632 if (!hasGeometry) {
1633 newGeometry = QDebugDrawHelper::generateTriangleMeshGeometry(
1634 triangleMeshGeometry->triangleMesh);
1635 }
1636 }
1637 }
1638
1639 if (newGeometry) {
1640 delete holder.geometry;
1641 holder.geometry = newGeometry;
1642 }
1643
1644 model->setGeometry(holder.geometry);
1645 model->setVisible(true);
1646
1647 model->setRotation(sceneRotation);
1648 model->setPosition(scenePosition);
1649 }
1650 }
1651
1652 // Remove old debug models
1653 m_DesignStudioDebugModels.removeIf(
1654 [&](QHash<QPair<QAbstractCollisionShape *, QAbstractPhysicsNode *>,
1655 DebugModelHolder>::iterator it) {
1656 if (!currentCollisionShapes.contains(it.key())) {
1657 auto holder = it.value();
1658 holder.releaseMeshPointer();
1659 if (holder.model) {
1660 delete holder.geometry;
1661 delete holder.model;
1662 }
1663 return true;
1664 }
1665 return false;
1666 });
1667}
1668
1669void QPhysicsWorld::disableDebugDraw()
1670{
1671 m_hasIndividualDebugDraw = false;
1672
1673 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1674 if (!body->frontendNode)
1675 continue;
1676
1677 const auto &collisionShapes = body->frontendNode->getCollisionShapesList();
1678 const int length = collisionShapes.length();
1679 for (int idx = 0; idx < length; idx++) {
1680 const auto collisionShape = collisionShapes[idx];
1681 if (collisionShape->enableDebugDraw()) {
1682 m_hasIndividualDebugDraw = true;
1683 return;
1684 }
1685 }
1686 }
1687}
1688
1689void QPhysicsWorld::setEnableCCD(bool enableCCD)
1690{
1691 if (m_enableCCD == enableCCD)
1692 return;
1693
1694 if (m_physicsInitialized) {
1695 qWarning()
1696 << "Warning: Changing 'enableCCD' after physics is initialized will have no effect";
1697 return;
1698 }
1699
1700 m_enableCCD = enableCCD;
1701 emit enableCCDChanged(m_enableCCD);
1702}
1703
1704void QPhysicsWorld::setTypicalLength(float typicalLength)
1705{
1706 if (qFuzzyCompare(typicalLength, m_typicalLength))
1707 return;
1708
1709 if (!qIsFinite(typicalLength) || typicalLength <= 0.f) {
1710 qWarning() << "Warning: 'typicalLength' value less than zero, ignored";
1711 return;
1712 }
1713
1714 if (m_physicsInitialized) {
1715 qWarning() << "Warning: Changing 'typicalLength' after physics is initialized will have "
1716 "no effect";
1717 return;
1718 }
1719
1720 m_typicalLength = typicalLength;
1721
1722 emit typicalLengthChanged(typicalLength);
1723}
1724
1725void QPhysicsWorld::setTypicalSpeed(float typicalSpeed)
1726{
1727 if (qFuzzyCompare(typicalSpeed, m_typicalSpeed))
1728 return;
1729
1730 if (!qIsFinite(typicalSpeed) || typicalSpeed <= 0.f) {
1731 qWarning() << "Warning: 'typicalSpeed' must be finite and greater than zero, ignored";
1732 return;
1733 }
1734
1735 if (m_physicsInitialized) {
1736 qWarning() << "Warning: Changing 'typicalSpeed' after physics is initialized will have "
1737 "no effect";
1738 return;
1739 }
1740
1741 m_typicalSpeed = typicalSpeed;
1742
1743 emit typicalSpeedChanged(typicalSpeed);
1744}
1745
1746float QPhysicsWorld::defaultDensity() const
1747{
1748 return m_defaultDensity;
1749}
1750
1751float QPhysicsWorld::minimumTimestep() const
1752{
1753 return m_minTimestep;
1754}
1755
1756float QPhysicsWorld::maximumTimestep() const
1757{
1758 return m_maxTimestep;
1759}
1760
1761void QPhysicsWorld::setDefaultDensity(float defaultDensity)
1762{
1763 defaultDensity = qBound(0.0000001f, defaultDensity, PX_MAX_F32);
1764
1765 if (qFuzzyCompare(m_defaultDensity, defaultDensity))
1766 return;
1767 m_defaultDensity = defaultDensity;
1768
1769 // Go through all dynamic rigid bodies and update the default density
1770 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1771 if (body->frontendNode)
1772 body->updateDefaultDensity(m_defaultDensity);
1773 }
1774
1775 emit defaultDensityChanged(defaultDensity);
1776}
1777
1778// Remove physics world items that no longer exist
1779
1780void QPhysicsWorld::cleanupRemovedNodes()
1781{
1782 m_physXBodies.removeIf([this](QAbstractPhysXNode *body) {
1783 return body->cleanupIfRemoved(m_physx);
1784 });
1785 m_removedPhysicsNodes.clear();
1786}
1787
1788// Tell the triggers which nodes have had their shapes replaced, and drop what
1789// the reports that followed the last such replacement did not mention
1790
1791void QPhysicsWorld::invalidateTriggerOverlaps(QSpan<QAbstractPhysXNode *const> rebuiltBodies)
1792{
1793 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1794 auto *trigger = qobject_cast<QTriggerBody *>(body->frontendNode);
1795 if (!trigger)
1796 continue;
1797 for (QAbstractPhysXNode *rebuilt : rebuiltBodies) {
1798 // The rebuilt nodes are held as backends, which outlive the frame,
1799 // since syncing the frame can run a handler that deletes a node
1800 // that was rebuilt earlier in it. deregisterNode() takes the
1801 // frontend off the backend of whatever is deleted.
1802 if (rebuilt->frontendNode)
1803 trigger->invalidateOverlaps(rebuilt->frontendNode);
1804 }
1805 }
1806
1807 m_triggerOverlapsInvalidated = true;
1808}
1809
1810void QPhysicsWorld::dropUnreportedTriggerOverlaps()
1811{
1812 // Nothing is waiting to be reported again unless shapes were replaced, so
1813 // this stays off the frame of a scene that never rebuilds any.
1814 if (!m_triggerOverlapsInvalidated)
1815 return;
1816 m_triggerOverlapsInvalidated = false;
1817
1818 // A body reported as having left can be deleted from the handler, and so can
1819 // a trigger that has not been reached yet. deregisterNode() takes the
1820 // frontend off whatever is deleted, so what still has one here is what
1821 // is left.
1822 for (QAbstractPhysXNode *body : std::as_const(m_physXBodies)) {
1823 if (auto *trigger = qobject_cast<QTriggerBody *>(body->frontendNode))
1824 trigger->dropUnreportedOverlaps();
1825 }
1826}
1827
1828void QPhysicsWorld::cleanupRemovedJoints()
1829{
1830 for (physx::PxJoint *joint : m_removedJoints) {
1831 if (joint)
1832 joint->release();
1833 }
1834 m_removedJoints.clear();
1835}
1836
1837void QPhysicsWorld::initPhysics()
1838{
1839 Q_ASSERT(!m_physicsInitialized);
1840
1841 const unsigned int numThreads = m_numThreads >= 0 ? m_numThreads : qMax(0, QThread::idealThreadCount());
1842 m_physx->createScene(m_typicalLength, m_typicalSpeed, m_gravity, this, numThreads);
1843 m_frameAnimator->start();
1844 m_physicsInitialized = true;
1845}
1846
1847void QPhysicsWorld::simulateFrame()
1848{
1849 constexpr double MILLIONTH = 0.000001;
1850 constexpr double THOUSANDTH = 0.001;
1851
1852 if (m_inDesignStudio) {
1853 frameFinishedDesignStudio();
1854 return;
1855 }
1856
1857 if (!m_physx->isRunning) {
1858 m_timer.start();
1859 m_physx->isRunning = true;
1860 const double minTimestepSecs = m_minTimestep * 0.001;
1861 m_physx->scene->simulate(minTimestepSecs);
1862 m_currTimeStep = minTimestepSecs;
1863 return;
1864 }
1865
1866 // Frame not ready yet
1867 if (!m_frameFetched && !m_physx->scene->checkResults()) {
1868 return;
1869 }
1870
1871 // Frame ready, fetch and finish it
1872 if (!m_frameFetched) {
1873 m_physx->scene->fetchResults(true);
1874 frameFinished(m_currTimeStep);
1875 m_frameFetched = true;
1876 if (Q_UNLIKELY(!qtPhysicsTimingsFile.isEmpty())) {
1877 const double deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
1878 m_frameTimings.append(deltaMS);
1879 }
1880 }
1881
1882 // Assuming: 0 <= minTimestep <= maxTimestep
1883 const double deltaMS = m_timer.nsecsElapsed() * MILLIONTH;
1884 if (deltaMS < m_minTimestep)
1885 return;
1886 const double deltaSecs = qMin<double>(deltaMS, m_maxTimestep) * THOUSANDTH;
1887 m_timer.restart();
1888 m_physx->scene->simulate(deltaSecs);
1889 m_frameFetched = false;
1890 m_currTimeStep = deltaSecs;
1891}
1892
1893void QPhysicsWorld::frameFinished(float deltaTime)
1894{
1895 matchOrphanNodes();
1896 matchOrphanJoints();
1897
1898 // One round of reports has been fetched since the shapes rebuilt below were
1899 // replaced, so whatever it did not mention is no longer overlapping. Kept
1900 // ahead of cleanupRemovedNodes(), like the contact callbacks, since a body
1901 // reported as having left can be deleted from the handler.
1902 dropUnreportedTriggerOverlaps();
1903
1904 emitContactCallbacks();
1905 cleanupRemovedNodes();
1906 cleanupRemovedJoints();
1907
1908 for (auto *node : std::as_const(m_newPhysicsNodes)) {
1909 auto *body = node->createPhysXBackend();
1910 body->init(this, m_physx);
1911 m_physXBodies.push_back(body);
1912 }
1913 m_newPhysicsNodes.clear();
1914
1915 QHash<QQuick3DNode *, QMatrix4x4> transformCache;
1916 QVarLengthArray<QAbstractPhysXNode *, 8> rebuiltBodies;
1917
1918 // TODO: Use dirty flag/dirty list to avoid redoing things that didn't change
1919 for (auto *physXBody : std::as_const(m_physXBodies)) {
1920 // Syncing runs a node's bindings, and a character controller reports
1921 // what it hits from here, so one can be deleted while this loop runs.
1922 if (!physXBody->frontendNode)
1923 continue;
1924
1925 physXBody->markDirtyShapes();
1926 const bool wasDirty = physXBody->shapesDirty();
1927 physXBody->rebuildDirtyShapes(this, m_physx);
1928
1929 // Rebuilding reports too, when it has to force a body kinematic.
1930 if (!physXBody->frontendNode)
1931 continue;
1932
1933 // Dirty before and clean after is the backend having replaced the shapes
1934 // of the node, which the triggers holding them need to hear about.
1935 if (wasDirty && !physXBody->shapesDirty())
1936 rebuiltBodies.append(physXBody);
1937 physXBody->updateFilters();
1938
1939 // Sync the physics world and the scene
1940 physXBody->sync(deltaTime, transformCache);
1941 }
1942
1943 if (!rebuiltBodies.isEmpty())
1944 invalidateTriggerOverlaps(rebuiltBodies);
1945
1946 for (QPhysicsJoint *joint : std::as_const(m_joints)) {
1947 joint->updatePhysXBackend();
1948 }
1949
1950 updateDebugDraw();
1951 emit frameDone(deltaTime * 1000);
1952}
1953
1954void QPhysicsWorld::frameFinishedDesignStudio()
1955{
1956 // Note sure if this is needed but do it anyway
1957 matchOrphanNodes();
1958 matchOrphanJoints();
1959 emitContactCallbacks();
1960 cleanupRemovedNodes();
1961 // Ignore new physics nodes, we find them from the scene node anyway
1962 m_newPhysicsNodes.clear();
1963
1964 updateDebugDrawDesignStudio();
1965}
1966
1967QPhysicsWorld *QPhysicsWorld::getWorld(QQuick3DNode *node)
1968{
1969 for (QPhysicsWorld *world : std::as_const(worldManager.worlds)) {
1970 if (!world->m_scene) {
1971 continue;
1972 }
1973
1974 QQuick3DNode *nodeCurr = node;
1975
1976 // Maybe pointless but check starting node
1977 if (nodeCurr == world->m_scene)
1978 return world;
1979
1980 while (nodeCurr->parentNode()) {
1981 nodeCurr = nodeCurr->parentNode();
1982 if (nodeCurr == world->m_scene)
1983 return world;
1984 }
1985 }
1986
1987 return nullptr;
1988}
1989
1990QPhysicsWorld *QPhysicsWorld::getWorld(QPhysicsJoint *joint)
1991{
1992 for (QPhysicsWorld *world : worldManager.worlds) {
1993 if (!world->m_scene) {
1994 continue;
1995 }
1996
1997 QObject *nodeCurr = joint;
1998
1999 while (nodeCurr->parent()) {
2000 nodeCurr = nodeCurr->parent();
2001 if (nodeCurr == world->m_scene) {
2002 return world;
2003 } else if (auto view3d = qobject_cast<QQuick3DViewport *>(nodeCurr);
2004 view3d && view3d->scene() == world->m_scene) {
2005 // HACK? if the parent is a view3d, check against its "implicit" scene
2006 return world;
2007 }
2008 }
2009 }
2010
2011 return nullptr;
2012}
2013
2014void QPhysicsWorld::matchOrphanNodes()
2015{
2016 // FIXME: does this need thread safety?
2017 if (worldManager.orphanNodes.isEmpty())
2018 return;
2019
2020 qsizetype numNodes = worldManager.orphanNodes.length();
2021 qsizetype idx = 0;
2022
2023 while (idx < numNodes) {
2024 auto node = worldManager.orphanNodes[idx];
2025 auto world = getWorld(node);
2026 if (world == this) {
2027 world->m_newPhysicsNodes.push_back(node);
2028 // swap-erase
2029 worldManager.orphanNodes.swapItemsAt(idx, numNodes - 1);
2030 worldManager.orphanNodes.pop_back();
2031 numNodes--;
2032 } else {
2033 idx++;
2034 }
2035 }
2036}
2037
2038void QPhysicsWorld::matchOrphanJoints()
2039{
2040 if (worldManager.orphanJoints.isEmpty())
2041 return;
2042
2043 qsizetype numNodes = worldManager.orphanJoints.length();
2044 qsizetype idx = 0;
2045
2046 while (idx < numNodes) {
2047 auto node = worldManager.orphanJoints[idx];
2048 auto world = getWorld(node);
2049 if (world == this) {
2050 world->m_joints.push_back(node);
2051 // swap-erase
2052 worldManager.orphanJoints.swapItemsAt(idx, numNodes - 1);
2053 worldManager.orphanJoints.pop_back();
2054 numNodes--;
2055 } else {
2056 idx++;
2057 }
2058 }
2059}
2060
2061void QPhysicsWorld::findPhysicsNodes()
2062{
2063 // This method finds the physics nodes inside the scene pointed to by the
2064 // scene property. This method is necessary to run whenever the scene
2065 // property is changed.
2066 if (m_scene == nullptr)
2067 return;
2068
2069 // Recursively go through all children and add all QAbstractPhysicsNode's
2070 QList<QQuick3DObject *> children = m_scene->childItems();
2071 while (!children.empty()) {
2072 auto child = children.takeFirst();
2073 if (auto converted = qobject_cast<QAbstractPhysicsNode *>(child); converted != nullptr) {
2074 // This should never happen but check anyway.
2075 if (converted->m_backendObject != nullptr) {
2076 qWarning() << "Warning: physics node already associated with a backend node.";
2077 continue;
2078 }
2079
2080 m_newPhysicsNodes.push_back(converted);
2081 worldManager.orphanNodes.removeAll(converted); // No longer orphan
2082 }
2083 children.append(child->childItems());
2084 }
2085}
2086
2087void QPhysicsWorld::emitContactCallbacks()
2088{
2089 for (const QPhysicsWorld::BodyContact &contact : std::as_const(m_registeredContacts)) {
2090 if (m_removedPhysicsNodes.contains(contact.sender)
2091 || m_removedPhysicsNodes.contains(contact.receiver))
2092 continue;
2093 contact.receiver->registerContact(contact.sender, contact.positions, contact.impulses,
2094 contact.normals);
2095 }
2096
2097 m_registeredContacts.clear();
2098}
2099
2100physx::PxPhysics *QPhysicsWorld::getPhysics()
2101{
2102 return StaticPhysXObjects::getReference().physics;
2103}
2104
2105const physx::PxCookingParams *QPhysicsWorld::getCookingParams()
2106{
2107 // PhysX cooks meshes through free functions taking the parameters, instead of through a
2108 // cooking object, but they still need the foundation for allocation and error reporting,
2109 // so there is nothing to cook with until it has been created.
2110 if (!StaticPhysXObjects::getReference().foundationCreated)
2111 return nullptr;
2112
2113 // FIXME: does the tolerance matter?
2114 static const physx::PxCookingParams params { physx::PxTolerancesScale() };
2115 return &params;
2116}
2117
2118physx::PxControllerManager *QPhysicsWorld::controllerManager()
2119{
2120 if (m_physx->scene && !m_physx->controllerManager) {
2121 m_physx->controllerManager = PxCreateControllerManager(*m_physx->scene);
2122 qCDebug(lcQuick3dPhysics) << "Created controller manager" << m_physx->controllerManager;
2123 }
2124 return m_physx->controllerManager;
2125}
2126
2127QQuick3DNode *QPhysicsWorld::scene() const
2128{
2129 return m_scene;
2130}
2131
2132void QPhysicsWorld::setScene(QQuick3DNode *newScene)
2133{
2134 if (m_scene == newScene)
2135 return;
2136
2137 m_scene = newScene;
2138
2139 // Delete all nodes since they are associated with the previous scene. One
2140 // deleted from a report earlier in this frame has nothing left to
2141 // deregister.
2142 for (auto body : std::as_const(m_physXBodies)) {
2143 if (body->frontendNode)
2144 deregisterNode(body->frontendNode);
2145 }
2146
2147 // Check if scene is already used by another world
2148 bool sceneOK = true;
2149 for (QPhysicsWorld *world : std::as_const(worldManager.worlds)) {
2150 if (world != this && world->scene() == newScene) {
2151 sceneOK = false;
2152 qWarning() << "Warning: scene already associated with physics world";
2153 }
2154 }
2155
2156 if (sceneOK)
2157 findPhysicsNodes();
2158 emit sceneChanged();
2159}
2160
2161int QPhysicsWorld::numThreads() const
2162{
2163 return m_numThreads;
2164}
2165
2166void QPhysicsWorld::setNumThreads(int newNumThreads)
2167{
2168 newNumThreads = qMin(newNumThreads, 256);
2169
2170 if (m_numThreads == newNumThreads)
2171 return;
2172
2173 m_numThreads = newNumThreads;
2174 emit numThreadsChanged();
2175}
2176
2177bool QPhysicsWorld::reportKinematicKinematicCollisions() const
2178{
2179 return m_reportKinematicKinematicCollisions;
2180}
2181
2182void QPhysicsWorld::setReportKinematicKinematicCollisions(
2183 bool newReportKinematicKinematicCollisions)
2184{
2185 if (m_reportKinematicKinematicCollisions == newReportKinematicKinematicCollisions)
2186 return;
2187 m_reportKinematicKinematicCollisions = newReportKinematicKinematicCollisions;
2188 emit reportKinematicKinematicCollisionsChanged();
2189}
2190
2191bool QPhysicsWorld::reportStaticKinematicCollisions() const
2192{
2193 return m_reportStaticKinematicCollisions;
2194}
2195
2196void QPhysicsWorld::setReportStaticKinematicCollisions(bool newReportStaticKinematicCollisions)
2197{
2198 if (m_reportStaticKinematicCollisions == newReportStaticKinematicCollisions)
2199 return;
2200 m_reportStaticKinematicCollisions = newReportStaticKinematicCollisions;
2201 emit reportStaticKinematicCollisionsChanged();
2202}
2203
2204QPhysicsWorld::QueryStructure QPhysicsWorld::staticQueryStructure() const
2205{
2206 return m_staticQueryStructure;
2207}
2208
2209void QPhysicsWorld::setStaticQueryStructure(QueryStructure newStaticQueryStructure)
2210{
2211 if (m_staticQueryStructure == newStaticQueryStructure)
2212 return;
2213
2214 if (newStaticQueryStructure == QueryStructure::NoStructure) {
2215 qWarning()
2216 << "Warning: The QueryStructure::NoStructure is not supported for staticQueryStructure. Available options: [StaticTree, DynamicTree]";
2217 return;
2218 }
2219
2220 if (m_physicsInitialized) {
2221 qWarning()
2222 << "Warning: Changing 'staticQueryStructure' after physics is initialized will have no effect";
2223 return;
2224 }
2225
2226 m_staticQueryStructure = newStaticQueryStructure;
2227 emit staticQueryStructureChanged();
2228}
2229
2230QPhysicsWorld::QueryStructure QPhysicsWorld::dynamicQueryStructure() const
2231{
2232 return m_dynamicQueryStructure;
2233}
2234
2235void QPhysicsWorld::setDynamicQueryStructure(QueryStructure newDynamicQueryStructure)
2236{
2237 if (m_dynamicQueryStructure == newDynamicQueryStructure)
2238 return;
2239
2240 if (m_physicsInitialized) {
2241 qWarning()
2242 << "Warning: Changing 'dynamicQueryStructure' after physics is initialized will have no effect";
2243 return;
2244 }
2245
2246 m_dynamicQueryStructure = newDynamicQueryStructure;
2247 emit dynamicQueryStructureChanged();
2248}
2249
2250bool QPhysicsWorld::testRaycastQuery(const QVector3D &origin,
2251 const QVector3D &direction,
2252 float maxDistance,
2253 bool includeStatic,
2254 bool includeDynamic) const
2255{
2256 physx::PxRaycastBuffer hitBuffer;
2257 return raycastImpl(m_physx->scene, origin, direction,
2258 maxDistance, includeStatic, includeDynamic,
2259 hitBuffer,
2260 physx::PxHitFlags(),
2261 physx::PxQueryFlag::eANY_HIT);
2262}
2263
2264QQuick3DPhysicsLocationHit QPhysicsWorld::singleRaycastQuery(const QVector3D &origin,
2265 const QVector3D &direction,
2266 float maxDistance,
2267 bool includeStatic,
2268 bool includeDynamic) const
2269{
2270 physx::PxRaycastBuffer hitBuffer;
2271 const bool status = raycastImpl(m_physx->scene, origin, direction,
2272 maxDistance, includeStatic, includeDynamic,
2273 hitBuffer,
2274 physx::PxHitFlag::eDEFAULT);
2275
2276 if (status && hitBuffer.hasBlock) {
2277 return QQuick3DPhysicsLocationHit(hitBuffer.block,
2278 hitBuffer.block);
2279 }
2280
2281 return {};
2282}
2283
2284QList<QQuick3DPhysicsLocationHit> QPhysicsWorld::multiRaycastQuery(const QVector3D &origin,
2285 const QVector3D &direction,
2286 float maxDistance,
2287 bool includeStatic,
2288 bool includeDynamic) const
2289{
2290 DynamicQueryCallback<physx::PxRaycastCallback,
2291 QQuick3DPhysicsLocationHit,
2292 physx::PxRaycastHit> hitBuffer;
2293
2294 if (raycastImpl(m_physx->scene, origin, direction,
2295 maxDistance, includeStatic, includeDynamic,
2296 hitBuffer,
2297 physx::PxHitFlag::eDEFAULT,
2298 physx::PxQueryFlag::eNO_BLOCK)) {
2299
2300 return hitBuffer.hits;
2301 }
2302
2303 return {};
2304}
2305
2306bool QPhysicsWorld::testSweepQuery(QAbstractCollisionShape *shape,
2307 const QVector3D &direction,
2308 float maxDistance,
2309 bool includeStatic,
2310 bool includeDynamic) const
2311{
2312 physx::PxSweepBuffer hitBuffer;
2313 return sweepImpl(m_physx->scene, shape, direction,
2314 maxDistance, includeStatic, includeDynamic,
2315 hitBuffer,
2316 physx::PxHitFlags(),
2317 physx::PxQueryFlag::eANY_HIT);
2318}
2319
2320QQuick3DPhysicsLocationHit QPhysicsWorld::singleSweepQuery(QAbstractCollisionShape *shape,
2321 const QVector3D &direction,
2322 float maxDistance,
2323 bool includeStatic,
2324 bool includeDynamic) const
2325{
2326 physx::PxSweepBuffer hitBuffer;
2327 const bool status = sweepImpl(m_physx->scene, shape,
2328 direction, maxDistance, includeStatic, includeDynamic,
2329 hitBuffer,
2330 physx::PxHitFlag::eDEFAULT | physx::PxHitFlag::eMTD);
2331
2332 if (status && hitBuffer.hasBlock) {
2333 return QQuick3DPhysicsLocationHit(hitBuffer.block, hitBuffer.block);
2334 }
2335
2336 return {};
2337}
2338
2339QList<QQuick3DPhysicsLocationHit> QPhysicsWorld::multiSweepQuery(QAbstractCollisionShape *shape,
2340 const QVector3D &direction,
2341 float maxDistance,
2342 bool includeStatic,
2343 bool includeDynamic) const
2344{
2345 DynamicQueryCallback<physx::PxSweepCallback,
2346 QQuick3DPhysicsLocationHit,
2347 physx::PxSweepHit> hitBuffer;
2348
2349 if (sweepImpl(m_physx->scene, shape,
2350 direction, maxDistance, includeStatic, includeDynamic,
2351 hitBuffer,
2352 physx::PxHitFlag::eDEFAULT | physx::PxHitFlag::eMTD,
2353 physx::PxQueryFlag::eNO_BLOCK)) {
2354 return hitBuffer.hits;
2355 }
2356
2357 return {};
2358}
2359
2360bool QPhysicsWorld::testOverlapQuery(QAbstractCollisionShape *shape,
2361 bool includeStatic,
2362 bool includeDynamic) const
2363{
2364 physx::PxOverlapBuffer hitBuffer;
2365 return overlapImpl(m_physx->scene,
2366 shape, includeStatic, includeDynamic,
2367 hitBuffer,
2368 physx::PxQueryFlag::eANY_HIT);
2369}
2370
2371QList<QQuick3DPhysicsQueryHit> QPhysicsWorld::multiOverlapQuery(QAbstractCollisionShape *shape,
2372 bool includeStatic,
2373 bool includeDynamic) const
2374{
2375 DynamicQueryCallback<physx::PxOverlapCallback,
2376 QQuick3DPhysicsQueryHit,
2377 physx::PxOverlapHit> hitBuffer;
2378
2379 if (overlapImpl(m_physx->scene,
2380 shape, includeStatic, includeDynamic,
2381 hitBuffer,
2382 physx::PxQueryFlag::eNO_BLOCK)) {
2383 return hitBuffer.hits;
2384 }
2385
2386 return {};
2387}
2388
2390
2391#include "qphysicsworld.moc"
Definition qlist.h:82
The QVector3D class represents a vector or vertex in 3D space.
Definition qvectornd.h:178
PxTransformT< float > PxTransform
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
static QWorldManager worldManager
static bool validateQuery(const physx::PxScene *scene, bool includeStatic, bool includeDynamic)
static const physx::PxGeometry * validateQueryShape(const physx::PxScene *scene, QAbstractCollisionShape *shape, bool includeStatic, bool includeDynamic)
static void collectPhysicsNodes(QQuick3DObject *node, QList< QAbstractPhysicsNode * > &nodes)
static const QString qtPhysicsTimingsFile
static bool raycastImpl(const physx::PxScene *scene, const QVector3D &origin, const QVector3D &direction, float maxDistance, bool includeStatic, bool includeDynamic, physx::PxRaycastCallback &hitCallback, physx::PxHitFlags hitFlags, physx::PxQueryFlags extraQueryFlags={})
static bool sweepImpl(const physx::PxScene *scene, QAbstractCollisionShape *shape, const QVector3D &direction, float maxDistance, bool includeStatic, bool includeDynamic, physx::PxSweepCallback &hitCallback, physx::PxHitFlags hitFlags, physx::PxQueryFlags extraQueryFlags={})
static bool overlapImpl(const physx::PxScene *scene, QAbstractCollisionShape *shape, bool includeStatic, bool includeDynamic, physx::PxOverlapCallback &hitCallback, physx::PxQueryFlags extraQueryFlags={})
#define QT_BEGIN_NAMESPACE
#define QT_END_NAMESPACE
#define Q_OBJECT
#define emit
QVector< QAbstractPhysicsNode * > orphanNodes
QVector< QPhysicsWorld * > worlds
QVector< QPhysicsJoint * > orphanJoints