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